Gra 2D, część 6: Wykrywanie kolizji i obsługa jednostek

03.02.2010 - Marcin Milewski
TrudnośćTrudność

Pierwszą metodą, którą należy przedstawić jest IsAnyFieldBelowMe. Na początku mapujemy (przekształcamy, odwzorowujemy) położenie gracza na numer kafla na mapie. Dzięki temu będziemy mogli sprawdzić, z jakimi kaflami jednostka sąsiaduje. Zajmuje się tym wspomniana niedawno metoda GetCurrentTile. Uznaje ona, że środek jednostki (a konkretniej środek jej AABB) jest dobrym jej przybliżeniem. Ponieważ dane w klasie Level trzymane są do góry nogami, więc dodatkowo należy zadbać o odpowiednie przekształcenie współrzędnej na osi rzędnych. W metodzie IsAnyFieldBelowMe nie możemy już sobie pozwolić na takie przybliżenie. Sprawdzamy więc, czy zaszła kolizja z którymkolwiek z trzech kafli poniżej jednostki i zwracamy tę informację.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool Entity::IsAnyFieldBelowMe(double dt, LevelPtr level) const{
    size_t curr_x_tile, curr_y_tile;
    GetCurrentTile(&curr_x_tile, &curr_y_tile);
    for (int x = -1; x < 2; ++x) {
        if (level->Field(curr_x_tile + x, curr_y_tile + 1) 
            == FT::None) {
            continue;
        }
        Aabb field_aabb=level->GetFieldAabb(curr_x_tile+x, 
                                            curr_y_tile+1);
        if (GetNextVerticalAabb(dt).IsOver(field_aabb)) {
            return true;
        }
    }
    return false;
}
  

Pozostałe metody sprawdzające sytuację, w której znalazła się jednostka, są w swoim działaniu analogiczne do przedstawionej. Zobaczmy, jak korzystać z tych funkcji, czyli jak zmieniać stan jednostki w zależności od otoczenia. Każda jednostka będzie na swój sposób reagowała na otaczający ją świat, a przedstawiona tu implementacja jest pewnym proponowanym zachowaniem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Entity::CheckCollisionsWithLevel(double dt, LevelPtr level){
    // czy jednostka koliduje z czymś od góry
    if(IsAnyFieldAboveMe(dt, level)) {
        Fall();  // rozpocznij spadanie
    }
 
    // czy jednostka koliduje z czymś od dołu
    if(IsAnyFieldBelowMe(dt, level)) {
        EntityOnGround();   // zatrzymaj na podłożu
    }
 
    // czy jednostka koliduje z czymś po lewej stronie
    if(IsAnyFieldOnLeft(dt, level)) {
        NegateXVelocity();  // zawróć
    }
 
    // czy jednostka koliduje z czymś po prawej stronie
    if(IsAnyFieldOnRight(dt, level)) {
        NegateXVelocity();  // zawróć
    }
}
  

Obsługa kolizji ze światem odbywać się będzie na początku metody Update. Wygląda to następująco:

1
2
3
4
5
void Entity::Update(double dt, LevelPtr level) {
    // ustaw domyślny ruch i sprawdź czy co w świecie piszczy
    SetDefaultMovement();
    CheckCollisionsWithLevel(dt, level);
  

Przykłady wykorzystania klasy Entity można znaleźć w plikach PlayerBulletEntity.h oraz MushEntity.h, które opisują odpowiednio pocisk wystrzelony przez gracza oraz przykładową jednostkę w grze. Obie implementację pokazują, jak proste jest dodawanie nowych jednostek do naszej gry - o to nam właśnie chodziło. Dodając kolejne jednostki należy pamiętać, aby umieścić odpowienie definicje sprite'ów. Zmodyfikowany konstruktor klasy SpriteConfig wygląda teraz następująco:

Pokaż/ukryj kod
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
SpriteConfig::SpriteConfig() {
    Insert("player_right", 
        SpriteConfigData(DL::Player, 5, .2, 0, 4 * 32, 32, 32, true));
    Insert("player_left",  
        SpriteConfigData(DL::Player, 5, .2, 0, 5 * 32, 32, 32, true));
    Insert("player_stop",  
        SpriteConfigData(DL::Player, 1, .2, 0, 6 * 32, 32, 32, true));
 
    Insert("platform_left",  
        SpriteConfigData(DL::Foreground, 1, 1, 0, 1*32, 32, 32, true));
    Insert("platform_mid", 
        SpriteConfigData(DL::Foreground, 1, 1, 0, 2*32, 32, 32, true));
    Insert("platform_right",
        SpriteConfigData(DL::Foreground, 1, 1, 0, 3*32, 32, 32, true));
 
    Insert("mush_right", 
        SpriteConfigData(DL::Entity, 2, .3, 0, 11 * 32, 32, 32, true));
    Insert("mush_left",  
        SpriteConfigData(DL::Entity, 2, .3, 0, 11 * 32, 32, 32, true));
    Insert("mush_stop",  
        SpriteConfigData(DL::Entity, 1, .3, 0, 12 * 32, 32, 32, true));
 
    Insert("player_bullet",  
        SpriteConfigData(DL::Entity, 2, .2, 3*32, 11*32, 32, 32, true));
}
  

Eliminowanie wrogich jednostek przy pomocy broni palnej jest zadaniem prostym. Postanawiamy więc, że zabicie przeciwnika przez naskoczenie na niego będzie dwukrotnie wyżej punktowane niż zastrzelenie go. Implementacja takiej kolizji wygląda następująco:

1
2
3
4
5
6
7
void Player::CollisionUnderPlayer(EntityPtr entity) {
    AllowToJump();
    Jump(GetDefaultYVelocity() + 6);
    AddScores(entity->GetScoresWhenKilled() * 2);
    entity->KilledByPlayer();
}
  

Wystarczy tylko we właściwym momencie wywołać tę metodę, aby gracz dostał punkty, a jednostka przestała istnieć. Nad poprawną kolejnością wywoływania metod zmieniających stan jednostek i gracza czuwa metoda Update w klasie Game.

Pokaż/ukryj kod
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
bool Game::Update(double dt) {
    // zbierz nowe obiekty z już istniejących
    ExecuteCreators();
 
    // ustaw domyślny ruch graczowi
    m_player->SetDefaultMovement();
 
    // sprawdź kolizje gracz-jednostka oraz jednostka-jednostka
    CheckPlayerEntitiesCollisions(dt);
    CheckEntityEntityCollisions(dt);
 
    // uaktualnij obiekt reprezentujący gracza
    m_player->Update(dt, m_level);
 
    // uaktualnij stan jednostek
    for (std::vector<EntityPtr>::iterator it=m_entities.begin();
        it != m_entities.end(); 
        ++it) {
        EntityPtr e = *it;
        if (e->IsAlive()) {
            e->Update(dt, m_level);
        }
    }
 
    // usuń niepotrzebne jednostki i dodaj nowe
    SeepAndAddEntities(dt);
 
    return !IsDone();
}
  

Metoda CheckPlayerEntitiesCollisions sprawdza, czy nastąpiła kolizja gracza z którąś z jednostek. Jeżeli tak, to podejmuje odpowiednią akcję, w zależności od miejsca wystąpienia kolizji.

Pokaż/ukryj kod
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
void Game::CheckPlayerEntitiesCollisions(double dt) {
    // poziomy i pionowy aabb gracza w następnym 'kroku'
    Aabb player_box_x = m_player->GetNextHorizontalAabb(dt);
    Aabb player_box_y = m_player->GetNextVerticalAabb(dt);
 
    for (std::vector<EntityPtr>::iterator it = m_entities.begin(); 
        it != m_entities.end(); 
        ++it) {
        EntityPtr entity = *it;
        if (entity->GetType() == ET::PlayerBullet) {
            continue;
        }
        entity->SetDefaultMovement();
        Aabb entity_box = entity->GetAabb();
 
        // sprawdź, czy wystąpiła kolizja. Jeżeli tak, to gracz
        // zdecyduje o losie swoim i jednostki. Zauważmy, 
        // że jeżeli wystąpi kolizja poniżej gracza 
        // (naskoczenie na jednostkę), to pozostałe nie będą 
        // sprawdzane.
        if (player_box_y.IsOver(entity_box)) {
            // naskoczenie na jednostkę
            m_player->CollisionUnderPlayer(entity);
            player_box_y = m_player->GetNextVerticalAabb(dt);
        }
        else if (player_box_x.IsOnLeftOf(entity_box)) {
            m_player->ForbidGoingRight();
            m_player->CollisionOnRight(entity);
            player_box_x = m_player->GetNextHorizontalAabb(dt);
        }
        else if (player_box_x.IsOnRightOf(entity_box)) {
            m_player->ForbidGoingLeft();
            m_player->CollisionOnLeft(entity);
            player_box_x = m_player->GetNextHorizontalAabb(dt);
        }
        else if (player_box_y.IsUnder(entity_box)) {
            m_player->Fall();
            m_player->CollisionOverPlayer(entity);
            player_box_y = m_player->GetNextVerticalAabb(dt);
        }
    }
}
  

Sprawdzenie występowania kolizji jednostka-jednostka wykonamy w podobny sposób - nie będzie nas interesować, z której strony wystąpiła kolizja, a jedynie czy w ogóle miała miejsce. Ponieważ w każdym momencie na planszy nie będzie zbyt dużej liczby jednostek, to testowanie kolizji zaimplementujemy na zasadzie każdy z każdym. Rozwiązanie nie jest najszybszym możliwym, ale za to... jest proste! Konkretną implementację można zobaczyć w metodzie Game::CheckEntityEntityCollisions, a przejrzyściej jest podać w tym miejscu algorytm zapisany w pseudokodzie:

1
2
3
4
5
6
  Dla każdej jednostki A ze zbioru jednostek
     Dla każdej jednostki B ze zbioru jednostek
        Jeżeli para (A, B) nie była jeszcze sprawdzana
           Jeżeli jednostki A oraz B żyją
              Obsłuż kolizję
  

Obsługiwaniem kolizji w naszej grze zajmuje się metoda CheckCollisionOfOnePair. Na początku sprawdza ona czy AABB jednostek się przecinają, jeżeli tak, to przechodzi do obsługi kolizji. Ponieważ nie wiemy, w jakiej kolejności zostaną do metody przekazane jednostki, a nie ma to dla nas znaczenia, definiujemy proste makro, które zapewni nam odpowiednią kolejność. Oto kompletny kod omawianej metody:

Pokaż/ukryj kod
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
void Game::CheckCollisionOfOnePair(EntityPtr fst_entity, 
                                   ET::EntityType fst_type,
                                   EntityPtr snd_entity, 
                                   ET::EntityType snd_type, 
                                   double dt) {
    if (fst_entity->GetNextAabb(dt).Collides(
        snd_entity->GetNextAabb(dt)) == false) {
        return;
    }
 
    // makro SWAP_IF pozwala zamienić znaczenie argumentów 
    // fst_* z snd_* w ten sposób, że sprawdzając kolizję 
    // otrzymujemy wskażniki zawsze w dobrej kolejności, 
    // tzn. tak, aby każdą parę obsługiwać jeden raz
#define SWAP_IF( type_a, type_b )  \
    if (fst_type == type_a && snd_type == type_b) {  \
        std::swap(fst_entity, snd_entity);   \
        std::swap(fst_type, snd_type);   \
    }
 
    SWAP_IF( ET::PlayerBullet, ET::Mush )
    // (...) kolejne wywołania SWAP_IF będą w tym miejscu
 
    // w tym miejscu wiemy, że jeżeli nastąpiła kolizja 
    // Mush z PlayerBullet, to jednostka Mush będzie pod 
    // fst_entity a PlayerBullet pod snd_entity. Poniżej 
    // reagujemy na konkretne kolizje.
 
    if (fst_type == ET::Mush && snd_type == ET::PlayerBullet) {
        snd_entity->SetIsDead(true);
        m_player->AddScores(fst_entity->GetScoresWhenKilled());
        fst_entity->KilledWithBullet();
    }
 
    if (fst_type == ET::Mush && snd_type == ET::Mush) {
        fst_entity->NegateXVelocity();
        snd_entity->NegateXVelocity();
    }
 
#undef SWAP_IF
}
  

W metodzie obsługi zdarzeń (ProcessEvents) w klasie Game pod klawisz 's' podpinamy akcję wystrzelenia pocisku przez gracza. Zmodyfikowany kawałek kodu wygląda następująco:

1
2
3
4
5
6
7
8
9
    // (...) obsługa klawiszy
    } else if (event.type == SDL_KEYUP 
            && event.key.keysym.sym == SDLK_d) {
        m_player->StopRunning();
    } else if (event.type == SDL_KEYDOWN 
            && event.key.keysym.sym == SDLK_s) {
        m_player->FireBullet();
    // dalsza obsługa klawiszy (...)
    

I oto w naszej grze mamy już możliwość dodawania jednostek do poziomu, a także testowania i obsługi kolizji z mapą oraz między nimi na wzajem. Istnieje wiele technik badających kolizje między obiektami. My, ze względu na szacowaną małą liczbę jednostek w jednym momencie na ekranie, zdecydowaliśmy się na jeden z prostszych modeli. W tym przypadku sprawdza się wyśmienicie! Kompletny kod źródłowy do tego artykułu wraz ze wszystkimi zmianami można znaleźć pod tym odsyłaczem.

Masz pytanie, uwagę? Zauważyłeś błąd? Powiedz o tym na forum.

Poprzedni artykuł - Odtwarzamy dźwięk Następny artykuł - Diabeł tkwi w szczegółach

Zadania dla dociekliwych

  1. Jeżeli ustawimy gracza np. w środku poziomu, to łatwo zauważyć, że wiele jednostek zostanie dodanych do gry (odległość od gracza jest dużo "na minusie"), po czym zostaną one usunięte (bo są zbyt daleko od gracza). Jak najprościej można to poprawić?
  2. Po wystrzeleniu pocisku przez gracza będzie on "podróżował" po poziomie niemal w nieskończoność. Dopisz funkcjonalność, która po pewnym czasie usunie pocisk z jednostek gry.
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com