Gra 2D, część 14: Lepszy respawn i kilka szczegółów

01.10.2012 - Marcin Milewski
TrudnośćTrudność

Teraz przejdziemy do ciekawszej części. Najpierw zajmiemy się zdefiniowaniem interfejsu w klasie Player, a następnie wykorzystamy go w Game. W takim momencie nasuwa się od razu pytanie: jak to ma działać? Zauważmy, że pierwsza ze wspomnianych klas jest zupełnie nieświadoma rozgrywanej gry. Wynika z tego, że jedyną możliwością komunikacji z grą jest przejście w stan oznaczający "bohater zginął, proszę go gdzieś przywrócić" i oczekiwanie na spełnienie tego życzenia przez instancję klasy Game. Ta z kolei wie tylko, że należy przemieścić bohatera, ale nie wie jak to zrobić, gdyż informacje o stanie postaci są zamknięte w klasie Player. Zatem, to klasa Game podejmuje decyzję o przywróceniu postaci (na jej prośbę), a potem zleca klasie Player respawn.

Podsumujmy krótko przebieg wydarzeń:

  1. Bohater traci życie, więc ustawia flagę "zginąłem, proszę mnie zrespawnować".
  2. Gra w każdym obiegu pętli sprawdza czy postać ma ustawioną wspomnianą flagę.
  3. Jeśli flaga jest ustawiona, to wykonuje animację, przenosi gracza do miejsca zapisu, a następnie nakazuje postaci przywrócić (częściowo) stan. Może tak zrobić, gdyż w momencie zapisu zapamiętała w jakim stanie znajdował się bohater.
  4. Respawn zostaje zakończony i gracz kontynuuje rozgrywkę.

Skoro omówiliśmy już problem, to czas zabrać się za implementację. Do klasy Player dodajemy zapowiadaną dwukrotnie flagę, czyli pole m_should_be_respawned, a razem z nim metodę je odczytująca -- ShouldBeRespawned.

1
2
3
4
5
6
7
8
// Plik: Player.h
class Player : public Entity {
// (...)
private:
    bool m_should_be_respawned;  // czy gracz powinien zostać przywrócony
                                 // na zapisaną pozycję
public:
   bool ShouldBeRespawned() const   { return m_should_be_respawned; }

Następnie deklarujemy metodę RespawnFrom oraz podajemy jej definicję w pliku Player.cpp. Metoda ta jest odpowiedzialna za przywrócenie stanu odpowiednich pól (np. liczbę punktów chcemy zachować). Odczytujemy pozycję, informację o możliwości strzału, a parametry dotyczące poruszania się postaci ustawiamy na domyślne. Dzięki temu uzyskamy efekt jakby gracz wyskakiwał (tudzież wypadał) z portalu służącego do zapisu gry. Na koniec resetujemy flagę m_should_be_respawned.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Plik: Player.cpp
void Player::RespawnFrom(boost::shared_ptr<Player> saved_player) {
    m_state = PS::Stand;
    m_shooting_enabled = saved_player->CanShoot()
                      || saved_player->IsTwinShotEnabled();
    m_twin_shot_enabled = false;
    m_max_x_pos = saved_player->m_max_x_pos;
    SetPosition(saved_player->GetX(), saved_player->GetY());
    SetVelocity(0, 0);
    SetXAcceleration(saved_player->GetXAcceleration());
    SetYAcceleration(saved_player->GetYAcceleration());
    SetDefaultMovement();
    m_should_be_respawned = false;
}

Przejście do stanu "zginąłem, proszę mnie zrespawnować" odbywa się w metodzie LooseLife. Do tej pory wyglądała ona następująco:

1
2
3
4
5
6
7
8
9
10
11
12
// Plik: Player.cpp
void Player::LooseLife() {
    // utrata jednego życia
    m_lifes--;
 
    // nieśmiertelność przez pewien czas
    m_is_immortal = true;
    m_immortal_duration = 0;
 
    // ustaw graczowi nową pozycję (respawn)
    SetPosition(2, 2);
}  

Zmieniamy ostatni wiersz uzyskując:

1
2
3
4
5
6
7
8
9
10
11
void Player::LooseLife() {
    // utrata jednego życia
    m_lifes--;
 
    // nieśmiertelność przez pewien czas
    m_is_immortal = true;
    m_immortal_duration = 0;
 
    // zażądaj ustawienia gracza w punkcie zapisu                        // NOWE
    m_should_be_respawned = true;                                        // NOWE
}  

To wszystko, jeżeli chodzi o klasę Player. Teraz czekają nas zmiany w Game.
Zaczynamy od dodania testu czy nastąpiła kolizja bohatera z punktem zapisu. Analizując fragment metody Game::CheckPlayerEntitiesCollisions przedstawiony poniżej widzimy, że pozycja postaci jest zapisywana tylko wtedy, gdy nastąpiła kolizja z nieaktywną encją typu SavePoint. Pojawia się tam także funkcja boost::dynamic_pointer_cast -- spotkaliśmy się z nią już wcześniej, ale przypomnijmy, że jest do odpowiednik dynamic_cast jeżeli używamy inteligentnych wskaźników zamiast zwykłych.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Plik: Game.cpp, metoda CheckPlayerEntitiesCollisions
   } else if (entity_type == ET::Orb) {
      // gracz wziął kulę (oznaczającą dodatkowe punkty)
      if (m_player->GetAabb().Collides(entity->GetAabb())) {
         m_player->AddScores(score_for_orb);
         entity->SetIsDead(true);
      }
      continue;
   } else if (entity_type == ET::SavePoint) {                             // NOWE
      // gracz zapisał grę                                                // NOWE
      if (m_player->GetAabb().Collides(entity->GetAabb())) {              // NOWE
         SavePointPtr sp = boost::dynamic_pointer_cast<SavePoint>(entity);// NOWE
         if (sp && sp->IsInactive()) {                                    // NOWE
            SaveGame(m_player);                                           // NOWE
            sp->Activate();                                               // NOWE
         }                                                                // NOWE
      }                                                                   // NOWE
      continue;                                                           // NOWE
    }                                                                     // NOWE

W powyższym kodzie wywoływana jest funkcja SaveGame odpowiedzialna za zapisanie informacji istotnych do przywrócenia gracza we właściwe miejsce rozgrywanego poziomu. Aby przekonać się, które z nich są potrzebne należy przeanalizować kolejno wszystkie pola klasy Game (klasę Player analizowaliśmy wcześniej, pisząc metodę RespawnFrom, a poziom oraz jednostki nie ulegają zmianie w momemcie przywracania bohatera do gry, więc się nimi nie interesujemy). Okazuje się, że poza bohaterem ważne jest tylko pole m_stored_player_pos_x -- jest ono istotne podczas aktualizowania położenia kamery.
Do klasy Game dodajemy zatem wspomniane zmienne oraz metody:

1
2
3
4
5
6
7
// Plik: Game.h, sekcja private klasy Game
    void SaveGame(PlayerPtr player);
    void LoadGame();
 
    PlayerPtr m_saved_player;
    double m_saved_stored_player_pos_x;
    bool m_should_load_when_active_again;  
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com