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

03.02.2010 - Marcin Milewski
TrudnośćTrudność

Czy jednostki mogą tworzyć nowe jednostki?

W grze nie wszystkie jednostki można wczytać z pliku. Najprostszym przykładem jest pocisk wystrzeliwany przez gracza. Dopóki nie zostanie naciśnięty odpowiedni klawisz, nie wiemy, w którym miejscu poziomu go umiejscowić. Oto mechanizm, który pozwala na dodawanie nowych jednostek do gry nie tworząc zależności cyklicznych w kodzie.

Zależność cykliczna pojawia się w momencie, gdy plik A dołącza plik B, a plik B wymaga (być może pośrednio) pliku A. Ten problem pojawia się co jakiś czas podczas pisania programów w C++. Jednym z rozwiązań jest dodanie zapowiedzi klasy (ang. forward declaration).

Technika, która za chwilę zostanie przedstawiona ma znacznie szersze zastosowanie niż dodawanie nowych jednostek do gry. Można wykonywać dowolną akcję, którą udostępnia klasa Game. Możliwe jest także przekazywanie referencji do innych klas, w zależności od tego, co będzie nam potrzebne. Prezentowana tutaj koncepcja jest bardzo elastyczna, więc warto dobrze jej się przyjrzeć i stosować ją w swoich grach.

Klasę, która będzie pozwalała na wykonywanie akcji na obiekcie klasy Game nazwiemy Creator (będzie to klasa abstrakcyjna). Klasy po niej dziedziczące będą implementowały metodę Create odpowiedzialną za dodawanie nowych jednostek do gry lub wywołanie innych akcji na obiekcie game przekazanym przez referencję.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _CREATOR_H_INCLUDED__
#define _CREATOR_H_INCLUDED__
#include <boost/shared_ptr.hpp>
 
class Game;
 
class Creator {
public:
    virtual void Create(Game& game) = 0;
};
 
typedef boost::shared_ptr<Creator> CreatorPtr;
#endif
  

Przykładowa klasa dziedzicząca po Creator, to PlayerBulletCreator. Jest ona odpowiedzialna za tworzenie pocisków wystrzeliwanych przez gracza.

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
// dodaje pocisk gracza
class PlayerBulletCreator : public Creator {
public:
    explicit PlayerBulletCreator(double x, double y, 
                                 double vx, double vy)
        : m_x(x), m_y(y), m_vx(vx), m_vy(vy) {
    }
 
    void Create(Game& game) {
        // stwórz jednostkę i nadaj jej odpowiednią prędkość 
        // (zgodnie ze zwrotem postaci gracza)
        EntityPtr entity = Engine::Get().EntityFactory()->
                       CreateEntity(ET::PlayerBullet, m_x, m_y);
        double x_vel = m_vx < 0 
                            ? -entity->GetDefaultXVelocity() 
                            : entity->GetDefaultXVelocity();
        double y_vel = entity->GetDefaultYVelocity();
        entity->SetVelocity(x_vel, y_vel);
 
        // dodaj pocisk do gry oraz odegraj dźwięk 'laser'
        game.AddEntity(entity);
        Engine::Get().Sound()->PlaySfx("laser");
    }
 
private:
    double m_x, m_y;    // położenie
    double m_vx, m_vy;  // prędkość
};
  

Obiekty, które będą wykorzystywały klasę Creator do dodawania jednostek, powinny dziedziczyć po klasie CreatorProducer. Udostępnia ona metody do dodawania, usuwania oraz zwracania listy kreatorów. Oto jej definicja:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Klasa bazowa dla klas, które będą dodawać kreatory
class CreatorProducer {
public:
    void AddCreator(CreatorPtr creator) {
        m_creators.push_back(creator);
    }
 
    void DropAllCreators() {
        m_creators.clear();
    }
 
    std::list<CreatorPtr> GetCreators() const {
        return m_creators;
    }
 
private:
    std::list<CreatorPtr> m_creators;
};
  

W naszej grze klasami dziedziczącymi po CreatorProducer będą Player oraz Entity, których definicje poznamy wkrótce. Teraz zobaczmy jak łatwo jest odpytać wszystkie obiekty o chęć dodania nowych obiektów z wykorzystaniem przedstawianego mechanizmu.

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
void Game::ExecuteCreators() {
    std::list<CreatorPtr> creators;
 
    // pobierz kreatory od gracza
    std::list<CreatorPtr> plr_c = m_player->GetCreators();
    creators.splice(creators.end(), plr_c);
    m_player->DropAllCreators();
 
    // pobierz kreatory z jednostek
    for (std::vector<EntityPtr>::iterator it=m_entities.begin();
         it != m_entities.end(); 
         ++it) {
        EntityPtr e = *it;
        if (!e->IsDead()) {
            std::list<CreatorPtr> ent_c = e->GetCreators();
            creators.splice(creators.end(), ent_c);
            e->DropAllCreators();
        }
    }
 
    // uruchom wszystkie kreatory
    for (std::list<CreatorPtr>::iterator it = creators.begin();
         it != creators.end();
         ++it) {
        (*it)->Create(*this);
    }
}
  

Domyślamy się już jak będzie wyglądało tworzenie nowego kreatora. Oto metoda dodająca pocisk wystrzelony przez gracza:

1
2
3
4
5
6
7
8
9
10
11
void Player::FireBullet() {
    // GetX() oraz GetY() zwracają położenie lewego dolnego 
    // narożnika postaci. Zależnie od aktualnej prędkości postaci
    // dodajemy pocisk po odpowiedniej stronie.
    const double x = GetXVelocity() < 0 ? GetX()-.3 : GetX()+.7;
    const double y = GetY() + .5;
    AddCreator(CreatorPtr(
           new PlayerBulletCreator(
                  x, y, GetXVelocity(), GetYVelocity())));
}
  

Jak widać, dodawanie kreatorów jest bardzo proste. W powyższym przykładzie stałe zostały dobrane poniekąd drogą eksperymentu, jednak mają one ścisły związek z AABB postaci gracza oraz punktem, który uznajemy za jej położenie. Skoro mamy już wiedzę o tworzeniu pocisków oraz zapoznaliśmy się z teorią AABB, nadszedł czas, aby trochę postrzelać. Potrzebujemy do tego wykrywania kolizji pociski-jednostka oraz jej obsługi.

0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com