Gra 2D, część 8: Dobrze mieć wybór... poziomu

25.08.2010 - Marcin Milewski
TrudnośćTrudność

Przetwarzanie zdarzeń - poruszanie się po sieci połączeń

Kolejną ważną częścią implementacji nowego stanu jest napisanie kodu obsługującego zdarzenia. Nas interesują wyłącznie wiadomości, które nadejdą z klawiatury. To właśnie przy jej pomocy będzie obsługiwany ekran wyboru poziomu. Zanim przejdzemy do pisania kodu, rzućmy okiem na założenia dotyczące tego kawałka implementacji:

  • Do zatwierdzenie lokalizacji (rozpoczęcie poziomu) służy Enter.
  • Poruszanie odbywa się za pomocą strzałek.
  • Aby wprawić postać w ruch nie trzeba trzymać żadnych klawiszy - wystarczy jednokrotne naciśnięcie odpowiedniej strzałki
  • Można zmieniać kierunek ruchu postaci w trakcie poruszania.

Ogólny schemat konstruowania pętli do przetwarzania zdarzeń znamy z implementacji takich stanów jak Game, HallOfFame czy Menu. W LevelChoiceScreen będziemy obsługiwać następujące klawisze:

  • Strzałki - przemieszczanie postaci na ekranie.
  • Enter - zatwierdzanie poziomu (czyli wyrażenie chęci zagrania w dany poziom).
  • ESC - przejście do głównego menu.
Obsługa klawisz ESC jest krótka i raczej nie zmieni się w przyszłości, więc implementujemy ją bezpośrednio w metodzie ProcessEvents. Dla pozostałych dwóch tworzymy osobne funkcje do obsługi akcji z nimi związanymi.

Zobaczmy teraz implementację metody ProcessEvents w naszym stanie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void LevelChoiceScreen::ProcessEvents(const SDL_Event & event) {
    if (event.type == SDL_QUIT) {
        SetDone();
    }
 
    if (event.type == SDL_KEYDOWN) {
        SDLKey key = event.key.keysym.sym;
        if (key == SDLK_ESCAPE) {
            m_next_app_state = AppStatePtr(new MainMenu());
            SetDone();
        } else if (key == SDLK_LEFT) {
            GoLeft();
        } else if (key == SDLK_RIGHT) {
            GoRight();
        } else if (key == SDLK_UP) {
            GoUpward();
        } else if (key == SDLK_DOWN) {
            GoDown();
        } else if (key == SDLK_RETURN) {
            RunLevelFromNode();
        }
    }
}
  

Obsługa Entera

Zacznijmy od implementacji funkcji do obsługi Entera. Koncepcyjnie funkcja ta jest bardzo prosta: należy sprawdzić czy postać stoi w którymś z węzłów, a następnie utworzyć instancję nowego stanu (którym będzie wybrany poziom) i przekierować do niego sterowanie. To ostatnie odbywa się przez przypisanie wskaźnikowi m_next_app_state nowego stanu oraz ustawienie flagi is_done na wartosć true (wywołując metodę SetDone klasy bazowej) w celu oznaczenia aktualnego stanu jako zakończony. Szczegółami takimi jak inicjalizacja nowego stanu czy podmiana starego na nowy w odpowiednim momencie zajmie się klasa App (ta klasa towarzyszy nam niemal od początku cyklu artykułów; można go znaleźć w metodzie App::Run).

Oto kod metody, która zostanie wywołana po naciśnięciu Entera:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void LevelChoiceScreen::RunLevelFromNode() {
    // jeżeli postać jest w drodze (nie stoi w węźle),
    // to nie pozwalamy włączyć poziomu
    // (można zmienić wedle uznania)
    if (m_current_from_node != m_current_to_node) {
        return;
    }
 
    const std::string level_name = NodeToLevelName(m_current_to_node);
    boost::shared_ptr<LevelChoiceScreen>
      m_level_choice_screen(shared_from_this());
    Game* game_state = new Game(level_name, m_player);
    game_state->BindLevelChoiceScreen(m_level_choice_screen);
    m_next_app_state.reset(game_state);
 
    SetDone();  // metod klasy App
}
  

Metody do obsługi strzałek

Najważniejszym elementem przetwarzania wejścia w implementowanym stanie jest prawidłowa obsługa strzałek, aby móc sterować położeniem postaci na ekranie. Ponieważ poruszanie się we wszystkich czterech kierunkach jest niemal identyczne, to omówimy je na przykładzie poruszania się postaci w lewą stronę. W uwagach dotyczących naszego rozwiązania znajdują się punkty mówiące o tym, że rozpatrujemy dwie sytuacje: kiedy postać stoi w węźle oraz gdy znajduje się w ruchu.

W pierwszej z nich powinniśmy sprawdzić czy kierunek ruchu wybrany przez gracza jest poprawny, tzn. czy znajduje się na nim droga, którą można iść. Aby to stwierdzić wystarczy przejrzeć wszystkie krawędzie wychodzące z m_current_from_node. Jeżeli znajdziemy taką drogę, to ostawiamy m_current_to_node na końcowy węzeł znalezionej krawędzi. W przeciwnym przypadku nie wykonujemy żadnej akcji.

Druga sytuacja jest znacznie prostsza. Jeżeli postać znajduje się w ruchu, to wiemy że m_current_to_node != m_current_from_node. Wystarczy więc sprawdzić czy krawędź łącząca te węzły ma odpowiedni kierunek oraz zwrot. Odpowiedni oznacza, że jeżeli chcemy, aby postać poszła w lewo, to aktualnie musi iść w prawo (czyli krawędź jest pozioma oraz skierowana w prawo). Kiedy stwierdzimy, że możemy zmienić zwrot postaci, podmieniamy wartości m_current_from_node oraz m_current_to_node.

Ponieważ zapisanie opisanego algorytmu w języku C++ może być mało czytelne, to najpierw przygotowujemy algorytm w pseudokodzie. Przedstawiamy kompletny kod metody LevelChoiceScreen::GoLeft, a implementację metod do obsługi pozostałych trzech klawiszy strzałek pozostawiamy jako proste ćwiczenie.

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
bool LevelChoiceScreen::GoLeft() {
    // DZIAŁANIE
    //    jeżeli postać stoi w węźle
    //        sprawdź czy istnieje droga w lewo
    //        jeżeli tak
    //            ustaw to_node jako jej koniec
    //            zwróć true
    //        jeżeli nie
    //             zwróć false
    //    jeżeli postać idzie w prawo
    //        niech idzie w lewo
    //        return true
    //    return false
    //
 
    const Point from_node_pos = m_positions.at(m_current_from_node);
    const Point to_node_pos = m_positions.at(m_current_to_node);
 
    // czy postać stoi w węźle
    if (m_current_from_node == m_current_to_node) {
        // czy istnieje droga w lewo
        // dst_nodes – węzły, do których można dojść z from_node
        IntVector dst_nodes = m_connections.at(m_current_from_node); 
        for (IntVector::const_iterator it = dst_nodes.begin();
             it != dst_nodes.end(); ++it) {
            Point connection_node_pos = m_positions.at(*it);
            // czy krawędź łącząca węzły ma dobry kierunek oraz zwrot
            if (connection_node_pos[0] - from_node_pos[0] < 0) {
                // istnieje droga, którą można iść
                m_current_to_node = *it;
                return true;
            }
        }
        return false;
    }
 
    // czy postać idzie w prawo
    if (to_node_pos[0] - from_node_pos[0] > 0) {
        std::swap(m_current_from_node, m_current_to_node);
        return true;
    }
    return false;
}
    

Nowy stan można uznać za zaimplementowany. Wykonaliśmy najważniejszą pracę, teraz przyszedł czas na ostatnie szlify. Nasz nowy stan powinniśmy wpleść w już istniejące, gdyż dopiero wtedy będziemy mogli mówić o zakończeniu pracy.

0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com