Gra 2D, część 11: Edytor poziomów cz. 1

15.03.2011 - Marcin Milewski
TrudnośćTrudność

Nowe funkcje w klasie Level

Głównym zadaniem edytora jest modyfikowanie poziomu. Zatem będziemy musieli dopisać kilka metod, które do tej pory nie były potrzebne.

Pierwszą z nich jest zapisywanie pól planszy do pliku. Zadanie z kategorii tych łatwiejszych: iterujemy po dwuwymiarowej tablicy i zapisujemy wartość każdego pola do pliku. Każdy wiersz poziomu kończymy znakiem nowej linii -- z punktu widzenia gry nie jest to konieczne, bo białe znaki i tak są pomijane przy wczytywaniu. Pozwala to jednak przeglądać poziom w dowolnym edytorze tekstu, a kosztuje nas naprawdę niewiele dodatkowej pracy. Zauważmy, że przedstawiona poniżej implementacja formatuje wyjście tak, aby każde pole zajmowało 5 znaków. Wymaga to dołączenia nagłówka iomanip w pliku StdAfx.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Plik: Level.cpp
void Level::SaveFieldsToFile(const std::string& filename) {
    std::ofstream outfile(filename.c_str());
    if (!outfile) {
        std::cerr << "Nie udało się zapisać podłoża do pliku `"
                  << filename << "`. Problem z dostępem do pliku" << std::endl;
        return ;
    }
 
    outfile << GetWidth() << " " << GetHeight() << std::endl;
    for (size_t y = 0; y < GetHeight(); ++y) {
        for (size_t x = 0; x < GetWidth(); ++x) {
            outfile << std::setw(5)
                    << std::setfill(' ')
                    << m_data.at(y).at(x)
                    << " ";
        }
        outfile << std::endl;
    }
}
  

Siostrzaną funkcją, zapisującą dane dotyczące poziomu, jest SaveEntitiesToFile. Zapisuje ona informacje o jednostkach, bonusach, położeniu gracza, itp. Jej implementacja również jest prosta:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Plik: Level.cpp
void Level::SaveEntitiesToFile(const std::string& filename) {
    std::ofstream outfile(filename.c_str());
    if (!outfile) {
        std::cerr << "Nie udało się zapisać jednostek do pliku `"
                  << filename << "`. Problem z dostępem do pliku" << std::endl;
        return ;
    }
    outfile.precision(3);
    outfile << m_player_data.name << "\t"
            << m_player_data.x << "\t"
            << m_player_data.y << std::endl;
    for (std::list<LevelEntityData>::const_iterator it=m_entities_to_create.begin();
         it != m_entities_to_create.end(); ++it) {
        outfile << (*it).name << "\t" << (*it).x << "\t" << (*it).y << std::endl;
    }
    outfile.close();
}
  

Do tej pory tylko odczytywaliśmy dane z pliku, więc nie potrzebowaliśmy funkcji, które w jakiś sposób manipulowałyby instancją klasy Level. Najważniejszą funkcjonalnością edytora poziomów jest możliwość zmiany wybranego pola na inne. Realizują to poniższe wiersze kodu:

1
2
3
4
5
6
7
void Level::SetField(size_t x, size_t y, FT::FieldType ft) {
    if (x >= m_width || y >= m_height) {
        return ;
    }
    m_data.at(y).at(x) = ft;
}
  

Kolejne dwie metody będą manipulowały szerokością poziomu. Pierwsza obsłuży sytuację, kiedy użytkownik zechce dodać nowe pola za ostatnim klockiem po prawej stronie. Należy wtedy zwiększyć szerokość poziomu, jeżeli jest zbyt mała. Aby zwiększyć szerokość poziomu zwiększamy rozmiar każdego wiersza. Metoda EnsureWidth nie może zmniejszyć szerokości poziomu (zapewnia rozmiar równy co najmniej przekazanemu argumentowi). Na razie nie chcemy obsługiwać innej wysokości poziomu niż standardowa, czyli 20 klocków.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Level::EnsureWidth(size_t width) {
    if (GetWidth() >= width) {
        return;
    }
 
    assert(m_height > 0);  // póki co, program może zachowywać się dziwnie 
                           // dla niedodatniej wysokości poziomu.
    // std::cout << "resising from " << m_data.at(0).size() << " to " << width
                 << std::endl;
 
    for (size_t y = 0; y < m_height; ++y) {
        m_data.at(y).resize(width, FT::None);  // dodaj puste komórki
    }
    m_width = width;
}
  

Do zmniejszania szerokości poziomu wykorzystamy drugą ze wspomnianych metod manipulacji poziomem -- ShrinkWidth. Jej zadaniem jest przycięcie rozmiaru poziomu do wskazanego przez użytkownika. Specjalną wartością parametru jest 0 -- poziom powinien zostać przycięty automatycznie, czyli do najdalszego pola. Dlaczego nie bierzemy pod uwagę encji? Dlatego, że wydaje się to niepotrzebne -- czym kierowałby się projektant poziomu umieszczając np. bonus poza ostatnim polem? Gracz prawdopodobnie i tak nie mógłby z niego skorzystać (przypomnijmy, że aby przejść poziom należy wejść w pole typu FT::EndOfLevel), o estetyce takiego poziomu nie wspominając. Oto kod metody ShrinkWidth:

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
// Plik: Level.cpp
void Level::ShrinkWidth(size_t width) {
    // width == 0 oznacza automatyczne przycięcie
    if (width >= GetWidth()) {
        return ;
    }
    
    if (width > 0) {
        for (size_t y = 0; y < m_height; ++y) {
            m_data.at(y).resize(width);
        }
    } else { // automatyczne przycięcie -- znalezienie najdalszego pola poziomu
        size_t max_x = 0;
        for (size_t y = 0; y < m_height; ++y) {
            for (size_t x = 0; x < m_width; ++x) {
                if (m_data.at(y).at(x) != FT::None && x > max_x) {
                    max_x = x;
                }
            }
        }
 
        // TODO: znajdywanie najdalszej jednostki
 
        max_x++;
        m_width = max_x;
        // std::cout << "shrinking to " << max_x << std::endl;
        for (size_t y = 0; y < m_height; ++y) {
            m_data.at(y).resize(max_x);
        }
    }
}
  

Ostatnim usprawnieniem klasy Level będzie możliwość utworzenia nowej instancji na postawie już istniejącej, ale podając własną listę jednostek oraz encję gracza. Implementacja jest prosta, gdyż polega na przepisaniu wartości większości pól z przekazanej instancji klasy Level. Pozostałe pola wypełniamy wartością argumentów. Należy zadbać o sortowanie encji w poziomie. Oto odpowiedni kawałek kodu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Level::Level(LevelPtr level,
             const std::list<LevelEntityData>& entities_data,
             const LevelEntityData& player_data)
    : m_name(level->GetName()),
      m_width(level->GetWidth()),
      m_height(level->GetHeight()),
      m_data(level->m_data),
      m_entities_to_create(entities_data),
      m_player_data(player_data),
      m_loaded(true) {
    m_entities_to_create.sort(LevelEntityData());
    if (!(m_player_data.name == "player"
          && m_player_data.x > 0
          && m_player_data.y > 0)) {
        m_player_data.name = "player";
        m_player_data.x = 1;
        m_player_data.y = 19;
    }
}  

Nagłówki nowych metod należy dodać do pliku Level.h. Jego uaktualnioną zawartość można zobaczyć w kodzie końcowym dołączonym do tego artykułu.

0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com