DirectX: Trójwymiarowy labirynt od podstaw - Część II

17.03.2010 - Adam Błaszkiewicz
TrudnośćTrudność

Format pliku z mapą

Mapa będzie zapisana w pliku tekstowym (czyli znowu brzydko i nieefektywnie, ale za to prosto). W pierwszej linii pliku będą umieszczone wymiary labiryntu (3 liczby). W drugiej linii umieścimy pozycję początkową gracza (3 liczby). Następnie, będziemy wypisywać po kolei opisy każdego skrzyżowania (w jakiejś określonej kolejności, np y -> z -> x). Opisem skrzyżowania będzie 6 liczb, odpowiadających wszystkim możliwym drogom wychodzącym z tego skrzyżowania. Każda liczba będzie równa 1, jeśli przejście istnieje lub 0, jeśli przejścia nie ma. Przykładowy plik z mapą może wyglądać tak (za wyjątkiem komentarzy, które należałoby usunąć):

1
2
3
4
5
6
7
8
2 2 2   // wymiary labiryntu
0 0 0   // pozycja początkowa gracza
// "piętro" pierwsze:
0 1  0 1  0 1   1 0  0 0  0 0
0 0  1 0  0 0   0 0  0 0  0 0
// "piętro" drugie:
0 0  0 0  1 0   0 0  0 0  0 0
0 0  0 0  0 0   0 0  0 0  0 0

Wczytywanie mapy

Zadeklarujmy globalnie strumień, który będzie nam potrzebny do odczytu danych z pliku.

1
ifstream file;

W miejscu, gdzie mamy już utworzone urządzenie Direct3D, możemy wczytać mapę. Najpierw wczytamy wymiary mapy i rozszerzymy odpowiednio wektor mapa.

1
2
3
file.open( _T("mapa1.txt") );
file >> mapa_size[ 0 ] >> mapa_size[ 1 ] >> mapa_size[ 2 ];
mapa.resize( mapa_size[ 0 ]*mapa_size[ 1 ]*mapa_size[ 2 ] );

Następnie wczytujemy pozycję początkową gracza. Do zmiennej pozycja dodajemy dodatkowo 0.5 w każdym wymiarze, ponieważ ściany labiryntu umieścimy we współrzędnych całkowitych. Zmienną kierunek ustawiamy początkowo tak, aby gracz był skierowany w prawo. Wektor gora (oznaczający, w którą stronę skierowany jest "czubek głowy" gracza) ustawiamy na dodatnią oś y.

1
2
3
4
5
int x,y,z;
file >> x >> y >> z;
pozycja = D3DXVECTOR3(x + 0.5, y + 0.5, z + 0.5);
kierunek = D3DXVECTOR3(1,0,0);
gora = D3DXVECTOR3(0,1,0);

Typ D3DXVECTOR3 to po prostu matematyczny wektor zaimplementowany w Direct3D (moglibyśmy zamiast niego użyć własnej klasy).

Następnie tworzymy dwa bufory wierzchołków i blokujemy je, żeby za chwilę wypełnić je wierzchołkami:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
num_v = (mapa_size[ 0 ] + 1)*
        (mapa_size[ 1 ] + 1)*
        (mapa_size[ 2 ] + 1);
 
// Bufor wierzchołków ścian:
hr = pD3DDevice->CreateVertexBuffer(
        num_v * sizeof( Vertex ),
        0,
        FVF_VERTEX,
        D3DPOOL_DEFAULT,
        &pVBsciany,
        NULL );
if( FAILED( hr ) ) goto WinMain_end;
 
// Bufor wierzchołków podłóg:
hr = pD3DDevice->CreateVertexBuffer(
        num_v * sizeof( Vertex ),
        0,
        FVF_VERTEX,
        D3DPOOL_DEFAULT,
        &pVBpodlogi,
        NULL );
if( FAILED( hr ) ) goto WinMain_end;
 
Vertex *pVerticesS, *pVerticesP;
hr = pVBsciany->Lock( 0, 0, ( void** )&pVerticesS, 0 );
if( FAILED( hr ) ) goto WinMain_end;
 
hr = pVBpodlogi->Lock( 0, 0, ( void** )&pVerticesP, 0 );
if( FAILED( hr ) )
{
        pVBsciany->Unlock();
        goto WinMain_end;
}

Liczba wierzchołków w każdym wymiarze jest o 1 większa niż liczba skrzyżowań. Więc liczbą wszystkich wierzchołków będą wymiary labiryntu powiększone o 1 i przemnożone przez siebie.

Teraz musimy wypełnić bufory wierzchołków, a następnie odblokować je:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for(int z = 0; z <= mapa_size[ 2 ]; ++z)
for(int y = 0; y <= mapa_size[ 1 ]; ++y)
for(int x = 0; x <= mapa_size[ 0 ]; ++x)
{
        pVerticesS[ V( x, y, z ) ] =
                Vertex( (FLOAT)x, (FLOAT)y, (FLOAT)z,
                (FLOAT)(x+z), (FLOAT)(y) );
        pVerticesP[ V( x, y, z ) ] =
                Vertex( (FLOAT)x, (FLOAT)y, (FLOAT)z,
                (FLOAT)(x),   (FLOAT)(z) );
}
 
pVBsciany->Unlock();
pVBpodlogi->Unlock();

Do odwoływania się do elementów jednowymiarowej tablicy wierzchołków używamy wcześniej zdefiniowanego makra V.

Współrzędne tekstury w Direct3D są określone dwoma liczbami będącymi współrzędnymi piksela tekstury. (0,0) to jeden z rogów tekstury, a (1,1) to naprzeciwległy róg. Jeśli podamy współrzędną tekstury niezawierającą się w przedziale [0,1), współrzędna ta zostanie zapętlona w odpowiedni sposób, zgodnie z ustawieniami:


Przykładowa tekstura


Wrap

Mirror

Border

Clamp

Nas interesuje Wrap (ustawiony domyślnie) lub ewentualnie Mirror. Dla ścian (czyli kwadratów równoległych do osi Y), ustawiamy współrzędne tekstury na (x+z, y), aby każda ściana miała tak samo nałożoną teksturę. Zauważmy, że jeśli chcielibyśmy użyć tych wierzchołków do wyrenderowania podłóg, to ich współrzędne nie byłyby odpowiednie (odpowiednie, czyli (i, j), (i+1, j), (i+1, j+1), (i, j+1) dla pewnych i, j, idąc wzdłuż krawędzi podłogi/ściany; każda krawędź powinna łączyć wierzchołki o jednej współrzędnej tekstury takiej samej, a drugiej współrzędnej różnej o 1). Dlatego najprościej utworzyć 2 osobne bufory zawierające te same wierzchołki, ale ze zmienionymi współrzędnymi tekstury.

Natomiast dla podłóg ustawiamy współrzędne tekstury (x,z).

Wczytujemy mapę i przy okazji zliczamy ściany i podłogi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for(int z = 0; z < mapa_size[ 2 ]; ++z)
        for(int y = 0; y < mapa_size[ 1 ]; ++y)
        for(int x = 0; x < mapa_size[ 0 ]; ++x)
        for(int wymiar = 0; wymiar < 3; ++wymiar)
        for(int strona = 0; strona < 2; ++strona)
        {
                int b;
                file >> b;
                MAPA(x,y,z).przejscie[wymiar][strona] = b;
 
                if(!b && wymiar == 1) ++num_podlogi;
                if(!b && wymiar != 1) ++num_sciany;
        }
file.close();

Następnie trzeba wypełnić bufor indeksów:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
UINT *pS;
pD3DDevice->CreateIndexBuffer(
        num_sciany*2*3*sizeof( UINT ),
        D3DUSAGE_DONOTCLIP,
        D3DFMT_INDEX32,
        D3DPOOL_DEFAULT,
        &pIBsciany,
        NULL );
pIBsciany->Lock( 0U, 0U, (void**)&pS, 0 );
 
UINT *pP;
pD3DDevice->CreateIndexBuffer(
        num_podlogi*2*3*sizeof( UINT ),
        D3DUSAGE_DONOTCLIP,
        D3DFMT_INDEX32,
        D3DPOOL_DEFAULT,
        &pIBpodlogi,
        NULL );
pIBpodlogi->Lock( 0U, 0U, (void**)&pP, 0 );
        
for(int z = 0; z < mapa_size[ 2 ]; ++z)
for(int y = 0; y < mapa_size[ 1 ]; ++y)
for(int x = 0; x < mapa_size[ 0 ]; ++x)
for(int wymiar = 0; wymiar < 3; ++wymiar)
for(int strona = 0; strona < 2; ++strona)
if( !MAPA( x,y,z ).przejscie[ wymiar ][ strona ] )
{
        if( wymiar == 1 )
        {
                *pP++ = V(x,y+strona,z);
                *pP++ = V(x+strona,y+strona,z+1-strona);
                *pP++ = V(x+1-strona,y+strona,z+strona);
 
                *pP++ = V(x+1,y+strona,z+1);
                *pP++ = V(x+1-strona,y+strona,z+strona);
                *pP++ = V(x+strona,y+strona,z+1-strona);
        }
        else if( wymiar == 0 )
        {
                *pS++ = V(x+strona, y, z);
                *pS++ = V(x+strona, y+1-strona, z+strona);
                *pS++ = V(x+strona, y+strona, z+1-strona);
 
                *pS++ = V(x+strona, y+1, z+1);
                *pS++ = V(x+strona, y+strona, z+1-strona);
                *pS++ = V(x+strona, y+1-strona, z+strona);
        }
        else    // wymiar == 2
        {
                *pS++ = V(x, y, z+strona);
                *pS++ = V(x+1-strona, y+strona, z+strona);
                *pS++ = V(x+strona, y+1-strona, z+strona);
 
                *pS++ = V(x+1, y+1, z+strona);
                *pS++ = V(x+strona, y+1-strona, z+strona);
                *pS++ = V(x+1-strona, y+strona, z+strona);
        }
}
 
pIBsciany->Unlock();
pIBpodlogi->Unlock();

Na każdą ścianę przypadają 2 trójkąty, czyli w sumie 6 indeksów. W powyższym kodzie przechodzimy wszystkie połączenia między skrzyżowaniami i dodajemy ścianę, jeśli przejścia nie ma. Znowu używamy makra V, aby zdobyć pozycję wierzchołka w buforze wierzchołków dysponując jego współrzędnymi.

Następnie odnajdujemy w naszym kodzie miejsce, w którym ustawiamy parametr D3DRS_CULLMODE i zmieniamy jego wartość na D3DCULL_CCW. Oznacza to, że trójkąty będą widoczne tylko z jednej strony. CCW oznacza counter-clockwise, czyli kierunek podawania wierzchołków przeciwny do ruchu wskazówek zegara, jeśli patrzymy na trójkąt od strony, z której jest widoczny. Zauważmy, że w naszym labiryncie mogą powstać ściany, przez które da się przeniknąć w jedną stronę, ale wrócić już się nie da - w takim wypadku będą widoczne od tej strony, z której nie da się przez nie przeniknąć.

pD3DDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CCW );

Następnie używamy D3DX, aby utworzyć dwie tekstury - dla ściany i dla podłogi:

1
2
3
4
5
D3DXCreateTextureFromFile(
    pD3DDevice, _T("sciana.bmp"), &pTextureS );
 
D3DXCreateTextureFromFile(
    pD3DDevice, _T("podloga.bmp"), &pTextureP );

Nie zapomnijmy umieścić w folderze z grą plików sciana.bmp i podloga.bmp, najlepiej o wymiarach będących potęgami dwójki.

Należy również pamiętać, aby przed zakończeniem programu zwolnić interfejsy Direct3D, które właśnie utworzyliśmy:

1
2
3
4
5
6
7
8
if( pTextureS ) pTextureS->Release();
if( pTextureP ) pTextureP->Release();
 
if( pVBsciany ) pVBsciany->Release();
if( pVBpodlogi ) pVBpodlogi->Release();
 
if( pIBsciany ) pIBsciany->Release();
if( pIBpodlogi ) pIBpodlogi->Release();

5
Twoja ocena: Brak Ocena: 5 (1 ocena)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com