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
Tile-engine alapok 2009.11.24 00:13


Üdv mindenkinek!

Első és legfontosabb:
A minap nézegettem pár régi alapozó cikket, és iszonyatosan megrökönyödtem pár dologtól, amit
láttam. Rendben van, hogy alap szintű dolgok kezdőknek, de talán pont ezért nem volna szabad
hagyni, hogy mindenféle tipikus hibákat elkövessenek pusztán azért, mert követik az itteni
hibás leírást!
Úgy gondoltam ebből kiindulva, hogy egy gyors és funkcionális hiánypótló cikket publikálok,
mégpedig egy eléggé elévült(nek tűnő, de koránt sem az) témáról, a Tilemap-ről, vagy ahogy
Crusader nevezte a cikkében, csempézésről.

Absztrakt:
Nem szeretnék nyelvi specializációval élni, ezért úgy döntöttem, hogy csupán a logikáját,
működési elvét írom le, és csupán az érzékeltetés kedvéért mellékelek kódot is. Mivel a cikk
hirtelen felindulásból lett készítve, nem volt időm lerázni a rozsdát magamról, ami a C++ -t
illeti, és nem szeretnék ebből adódóan hibás sorokat írni, hát maradok a régi jó Visual Basic
-nél, ami -lévén, hogy majdnem natív angol- talán a legközérthetőbb.

Beszélnék arról, hogy egy Tile önmagában micsoda, hogyan épül fel egy Tilemap, illetve, hogy
ezt hogyan manipulálhatjuk. Megelőlegezem mindenkinek, aki tovább olvas, hogy elvégezte a 8
általánost, ennek fényében jöjjön hát a lényeg.

Íme a Tile:

Kód:
   <-8->
########^
########|
########8
########|
########·


Ez egy sima négyzet aminek van X,Y koordinátája a Síkon, illetve W,H szélessége és magassága.
Ez az alap. Ezen felül még rengeteg tulajdonsággal bírhat, szintén alap esetben pl típussal, ami
meghatározza a külsejét (textúra, animációs paraméterek, hozzáférhetőség, élettartam stb..), de
ez most a cikk szempontjából inszignifikáns faktor.

Mivel a Tilemap rengeteg Tile-ból állhat, így célszerű létrehozni a Tile-ok osztályát, bár én
most bőven megelégedek egy struktúrával is, ami VB-ben Type-ként van nevezve. Így hozzuk létre:

Kód:
Option Explicit

Public Type TILE
      X as single
      Y as Single
      W as Single
      H as single
End Type


Miután létrehoztuk a Tile-ok struktúráját, rögtön létre is hozhatjuk a Tile-okat. Én a deklarációkat
modulokban szoktam megejteni, tekintve, hogy sokkal áttekinthetőbb lesz a kód, ezért a Tile-ok
halmazát is Publikus változóként deklarálom (az előbbi struktúra is publikus):

Kód:
Public oTile(63) as TILE


Pontosan 64 darab Tile-nak foglaltam egy buffert (gyk: mivel nem írtam a kód elejére, hogy
Option Base 1, ezért 0-tól kezdődik a számlálás 1 helyett, ezért 63 az valójában 64). Ez így egy 8x8
méretű Tilemap-nek fog otthont adni.

Tekintve, hogy a világunk is egy nagy Tile, így őt is deklarálhatjuk az imént létrehozott struktúrából:

Kód:
Public oWorld as TILE
Public MaxTiles as byte


Mivel világból csak egy van, ezért neki elég egy egy elemű tömb. X, Y az ő esetében a világ gravitációs

középpontja, míg W és H az oszlopok és sorok száma lesz. A MaxTiles változó fogja nekünk evidenciában
tartani az összes Tile számát, azaz oWorld.W * oWorld.H értékét.


Egyéb deklarációkra is szükségünk van, főként az egér koordinátáira, mégpedig a scrollozás miatt.
Szintén Scroll miatt létrehozok egy logikai változót is, ami eldönti, hogy scrollozunk-e, vagy sem.

Kód:
Public Mx as integer 'egérváltozók
Public My as integer
Public MScroll as boolean

Public TmpX as integer 'átmeneti egérváltozók
Public TmpY as integer


Most, hogy a változókat deklaráltuk, elkezdhetjük inicializálni őket. Erre én kapásból rutint
használok (nem függvényt, mivel nincs visszatérési érték):

Kód:
Public Sub Initialize()

'beállítjuk a világ paramétereit
oWorld.X = 0
oWorld.Y = 0
oWorld.W = 8
oWorld.H = 8
 
'két lokális változó a feltöltő ciklushoz
dim i as byte: j as byte

'lokális index
dim n as byte
n = 0

'feltöltő ciklus
for i = 0 to oWorld.W
    for j = 0 to oWorld.H

         oTile(n).W = 8
         oTile(n).H = 8           
         oTile(n).X = i * oTile(n).W
         oTile(n).Y = j * oTile(n).H
         n = n + 1         
           
    next j
Next i

MaxTiles = n

End Sub


Ezt a rutint rögtön a Form_Load-ban meghívhatjuk. Nem csinál mást, mint az oWorld értékeit
beállítja, hogy az origó 0-án legyen, azaz a bal felső sarokban, illetve 8 sorral és 8 oszloppal
rendelkezzen.

Utána jön az egymásba ágyazott szerkezet, ami soronként és oszloponként végig megy a Tile-okon.
Először meghatározzuk minden Tile szélességét és magasságát. A példában abból indultam ki, hogy
pixel az alapegység, de ha twip-ben számolna a progi valamiért, akkor egy könnyed konverzióval
túltehetjük magunkat rajta:
TwipsPerPixel = 16, ezzel az értékkel kell a koordinátákat felszorozni, hogy pixelben kapjuk meg.

A belső ciklus számolja az oszlopban az elemeket (függőlegesen halad lefelé) minden vízszintes
értéknél, vagyis akkor lép egyet jobbra vízszintesen, amikor egy oszlopon teljesen végig haladt.
Pontosan a meghatározott 8 soron és 8 oszlopon megy végig, így a Tile-ok koordinátáit hozzá is
rendelhetjük ezen értékekhez, de mivel nem 1 pixel méretű egy Tile, hanem W és H méretű, ezért
X irányban W-vel, Y irányban pedig H-val kell megszorozni, így a következő Tile pontosan ott fog
kezdődni, ahol az előző véget ér.

Így, hogy megkaptuk a 2D-s rácsunkat, már rajtunk áll, hogy mit kezdünk vele. A lényeg, hogy a
ciklusok lepörgése után a használt lokális 'n' változóból megkapjuk, hogy pontosan mennyi Tile
van, jelen esetben 64. Ezt eltároljuk a MaxTiles-ban, hogy később ez alapján férjünk hozzá bármely
Tile-hoz az egymásba ágyazott ciklusok helyett, ami futási időben elég szignifikáns különbség!

Hozzuk létre a MAIN_Loop-ot, amit közvetlenül az Initialize() után hívhatunk meg:

Kód:
Public Sub MAIN_Loop()

'ez a fő ciklus, ami addig tart, amég ki nem lépünk
Do

'Ide jönnek a rutinok és függvények:
Draw_Tiles() 'Kirajzoló rutin

Get_Mouse_Scroll() 'Scrollozásért felelős rutin

'adott Tile = Get_Tile(Mx,My) 'így kapjuk meg a Tile-t az egér alatt

'a DoEvents kényszeríti a programot, hogy békén hagyja a gépünket amikor épp nincs mit csinálni,
'így nem fog lefagyni a programunk, amint belefut a ciklusba
DoEvents
Loop

End Sub


Létrehozom a kirajzoló rutin:

Kód:
Public Sub Draw_Tiles()

'lokális változó
dim n as byte   

'elszámolunk addig, ahány Tile-unk van
for n = 0 to MaxTiles

    'mivel csak azt a Tile-t szeretnénk megjeleníteni, amelyik épp a képen van, írnunk kell
    'egy erre vonatkozó képletet
    if Tile(n).X + Tile(n).W > 0 and Tile(n).X < me.width and Tile(n).Y + Tile(n).H > 0 _
    and Tile(n).Y < me.height then
   
        'a végén a 'B' paraméter jelzi, hogy nem vektort, hanem négyzetet rajzolunk   
         me.line (oTile(n).x + oWorld.X, oTile(n).y+oWorld.Y) _
            -(oTile(n).X + oTile(n).W + oWorld.X, oTile(n).Y + oTile(n).H + oWorld.Y),,B

       'látható, hogy a világ koordinátáját is hozzáadtuk minden Tile-hoz, így ők már nem egy fix
       'ponton lesznek elhelyezve, hanem relatív koordinátákon.   
   
    else
    'ha nincs a képen, akkor simán tovább lépünk

    end if


next n

End Sub


Ahoz, hogy scrollozzunk, nem kell mást csinálni, mint az adott ablak (adott esetben a form)
Mouse_Move eseményébe beleírunk egy értékátadást:

Kód:
Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)

Mx = X
My = Y

End Sub


Ez után megvizsgáljuk, hogy az egér (akár mozog, akár nem) a scrollozási tartományba esik-e. Ezt
az értéket nem határoztam meg előre, mert úgyis csak itt kell meghatározni, komolyabb programnál
természetesen ezt is előre deklaráltam volna.
Úgy vizsgáljuk meg, hogy írunk rá egy újabb rutint:

Kód:
Public Sub Get_Mouse_Scroll(X as integer, Y as integer)

'alapértelmezés szerint az MScroll hamis
MScroll = False

'a rutin megvizsgálja, hogy az egér koordinátái a kép szélén vannak-e, és ha igen, akkor
'megváltoztatja a világ koordinátáit. Minden esetben a scrollozás igaz lesz
if Mx <= 5 then oWorld.X = oWorld.X + 1: MScroll = True
if Mx >= me.width - 5 then oWorld.X = oWorld.X - 1: MScroll = True
if My <= 5 then oWorld.Y = oWorld.Y + 1: MScroll = True
if My >= me.height - 5 then oWorld.Y = oWorld.Y - 1: MScroll = True

End Sub


Ez a rutin is a MAIN_Loop-ból hívódik meg, hiszen valós időben kell figyelnünk a scrollozást is.
A Scrollozásnak van egy másik formája is, ami szerintem jobb, mint az előbbi. A Drag&Drop módszerről
beszélek, amivel bővíteni fogjuk az előzőeket:

Deklarálnunk kell legfölül egy MB változót:

Public MB as boolean


Ez fogja nekünk megmondani, hogy az egérgomb lenyomva van-e. Ezután a form Mouse_Down eseményébe bele
írjuk ezt:

Kód:
'egér lenyomva
MB = True
'átmeneti koordináták
TmpX = X
TmpY = Y
/[code]

A from Mouse_Up eseményében pedig feloldjuk:

[code]
MB = False


Ennek fényében kibővítjük a Scrollozó rutinunkat:

Kód:
Public Sub Get_Mouse_Scroll(X as integer, Y as integer)


'meghatározzuk, hogy az egérgomb lenyomva van-e, és csak akkor hajtódik végre a sima scroll, ha a feltétel
'nem teljesül, vagyis hamis
If MB = False then

   'alapértelmezés szerint az MScroll hamis
   MScroll = False

   'a rutin megvizsgálja, hogy az egér koordinátái a kép szélén vannak-e, és ha igen, akkor
   'megváltoztatja a világ koordinátáit. Minden esetben a scrollozás igaz lesz
   if Mx <= 5 then oWorld.X = oWorld.X + 1: MScroll = True
   if Mx >= me.width - 5 then oWorld.X = oWorld.X - 1: MScroll = True
   if My <= 5 then oWorld.Y = oWorld.Y + 1: MScroll = True
   if My >= me.height - 5 then oWorld.Y = oWorld.Y - 1: MScroll = True

else
   'ha a feltétel teljesül, vagyis az egérgomb lenyomva van, akkor ráfogtunk a Tilemap-re.
   'innentől kezdve a világ koordinátája aszerint változik, ahogy az egeret húzzuk, ezért be kell
   'vezetnünk a program elején két átmeneti egérváltozót (TmpX, TmpY).

   'amikor az egérgomb lenyomásra kerül, az MB után kell az egér aktuális értékét átadni ezeknek
   'a változóknak, mint kezdőértéket!
   'ezekből kiindulva:
   if Mx < TmpX then
        oWorld.X = oWorld.X - abs(TmpX-Mx)
      else
        oWorld.X = oWorld.X + abs(TmpX-Mx)
   end if
 
   If My < TmpY then
        oWorld.Y = oWorld.Y - abs(TmpY-My)
      else
        oWorld.Y = oWorld.Y + abs(TmpY-My)
   end if

   'a transzláció után természetesen az átmeneti koordinátákat frissíteni kell, hogy mindig az
   'elmozdulás mértékével mozdítsuk el a világot!
   TmpX = Mx
   TmpY = My

End If

End Sub


Jelenlegi cikkemben az utolsó hasznos függvény, ami talán a legtöbb problémát okozhat egy kezdő
számára, hogy hogyan kapjuk meg, hogy az egér melyik Tile fölött van. Ehez kell egy kis matek, de
íme a függvény, amit akkor hívunk meg, amikor szükségünk van az adott Tile-ra:

Kód:
Public Function Get_Tile(X as integer, Y as integer) as integer

'néhány lokális változó
dim rx as integer
dim ry as integer
dim iResult as integer

'a relatív koordinátát úgy kapjuk, hogy a világ koordinátáit elosztjuk a Tile-ok méretével
'és az eredményt kerekítjük.
rx = Fix((X - oWorld.X) / oTile(0).W) + 1
ry = Fix((Y - oWorld.Y) / oTile(0).H) + 1

'mivel 0-tól indul a számolás a sorokon és oszlopokon, ezért itt is 1-el kevesebb értékkel
'szorozzuk meg a világ maximális magasságát, így megkapjuk, hogy mennyi oszlop van teljesen
'az egér előtt, majd hozzáadunk még annyi sort az adott oszlopon, ahol az egér tart:

iResult = (rx - 1) * oWorld.H + ry

'mivel az egér lemehet a Tilemap-ről, ezért limitálnunk kell az eredményt:
if iResult < 1 then iResult = 1
if iResult > MaxTiles then iResult = MaxTiles

'az eredményt visszaadjuk a függvényhívó változónak
Get_Tile = iResult

End Function



A teljesség igénye nélkül azt hiszem, hogy minden lényeges dolgot leírtam. Ezek a dolgok nem csak
2D-s tilemap-nél működnek, illetve szükség szerint bővíthetőek. Remélem, hogy érthetőre sikerült
a cikk és némi segítséget nyújt azok számára, akiknek gőzük sincs róla, miként kell Tilemap-et
hegeszteni.

Köszönöm a figyelmet!

Értékelés: 7.33

Új hozzászólás
pgyalog          2009.12.30 09:05
petyur: szerintem csak akkor kezdj neki cikksorozatnak, ha már van kész tile-engine-en alapuló editorod, esetleg játékod, és mellékelsz hozzá .exe-t.
Sajna Bitsculptor cikkeből nem tudnék merre indulni, várnám a további leckéket, hogy lássak is az elméletből valamit.
petyur          2009.12.28 04:59
Sziasztok,

amennyiben van igény egy cikksorozatra a tile-mappingről, nekilátok egynek szívesen.
Ezzel dolgozom már jónéhány hónapja, talán lenne mit megosztanom a tapasztalataimról ezügyben.
Többrészes cikksorozatban gondolkodom, amelyben logikát és kódot is közölnék.
Vélemény? Van erre igény?
Asylum          2009.12.08 21:59
Szerintem amikor a tömb méretét adjuk meg akkor az elemszámot kell megadni nem az indexek maximumát.
mark576          2009.12.03 10:37
Azt sose értettem mi olyan egszerű ebben a dim as, by val as, sub end sub, meg ehhez hasonló szösszenetekben. Ennél még a C++ is könnyebb.
Bitsculptor          2009.11.27 07:57
sztem teljesen logikus, hogy 0-63 az 64
svn          2009.11.27 07:27
Már akinek szépség
Bitsculptor          2009.11.27 03:36
svn: a VB szépségei
svn          2009.11.27 02:28
Részletes és jó cikk . Bár a 63-as tömbön nagyon fogtam a fejem...
Bitsculptor          2009.11.25 06:10
Igazad van a mérettel kapcsolatban, abból indultam ki, hogy minden Tile egyforma méretű. A nagyobb területű dolgokat több Tile-on szoktam tárolni, nem a Tile méretét változtatom meg.

Ehm..lehet, hogy pár részletet nem magyaráztam, de nem sok cikket írtam eddig, és ezt is, ahogy írtam előljáróban, nagy hírtelen írtam meg reggel 6kor
Ha valami valakinek nem tiszta, akkor természetesen kifejtek mindent szívesen. A kódban levő kommentek is azon megfontolásból kerültek oda, hogy magyarázzák az elméletet. Mindenesetre köszönöm az észrevételt, a konzekvenciát levontam belőle a jövőre nézve
Matzi          2009.11.25 04:50
Nem akarok rosszindulatú lenni, de annak ellenére, hogy a logikát akartad leírni, és a kód csak szemléltetésnek van, a kód nélkül nem érthető, és (főleg a végére) már semmi nincs leírva magyarázatnak. Így mondjuk egy kezdő ki tudja másolni a kódot, de megérteni biztos nem fogja.
Ezen felül én a tile struktúrában nem tárolnék szélességeket, mert egészen vicces dolgokat produkálhat, ha nem konzisztensek, szóval egy tile legyen vagy fix méretű, vagy valahogy legyenek kezelve az eltérő méretű csempék rendesen.
Bitsculptor          2009.11.25 01:36
Na most erre reagálhatnék teljesen őszintén, de az nem illene ide
walkyrie          2009.11.25 01:26