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

03.02.2010 - Marcin Milewski
TrudnośćTrudność

Wykrywanie i obsługa kolizji

Na początku artykułu została podkreślona ważność mechanizmu wykrywania kolizji podczas tworzenia gry. W naszej grze oprócz kolizji gracz-podłoże, będziemy także sprawdzali kolizje jednostka-podłoże, jednostka-jednostka czy jednostka-pocisk.

Kiedy pomyślimy o poruszaniu się dowolnej jednostki, zauważymy istotne podobieństwo do postaci sterowanej przez gracza. Dlatego w naszej grze uznamy bohatera gracza za jednostkę, co wiąże się z tym, że część implementacji odpowiedzialna za poruszania będzie realizowana w klasie Entity, której podklasą będzie Player. Ponadto jednostka będzie udostępniała swój typ oraz liczbę punktów jaką należy przyznać graczowi za jej unicestwienie. Zauważmy, że choć jest to problematyczne (klasa gracza nie powinna zawierać takich metod!), to prowadzi do znacznego uproszczenia się kodu. Moglibyśmy tę wspólną część wyodrębnić w nieco inny sposób, jednak zależy nam, aby gra była prosta w swojej konstrukcji.

Implementowane przez nas jednostki będą domyślnie poruszały się ruchem jednostajnie przyspieszonym na każdej z osi. Zachowanie to oczywiście będzie można w prosty sposób zmienić w klasach dziedziczących po klasie Entity przez przepisanie metod GetNextXPosition, GetNextYPosition oraz ewentualne zmiany w Update jeżeli będzie taka potrzeba. Oto kawałek tej klasy odpowiedzialny za rzeczy ogólne:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Entity : public CreatorProducer {
public:
    explicit Entity(double x, double y, 
                    double def_velocity_x, 
                    double def_velocity_y,
                    double def_acceleration_x = 0, 
                    double default_acceleration_y = 0);
 
    virtual ET::EntityType GetType() const = 0;
    virtual int GetScoresWhenKilled() const { return 0; }
    virtual void Update(double dt, LevelPtr level);
    virtual void Draw() const;
    virtual void 
        CheckCollisionsWithLevel(double dt, LevelPtr level);
    void SetSprites(SpritePtr left, 
                    SpritePtr right, 
                    SpritePtr stop);
    bool IsDead() const                { return m_is_dead; }
    bool IsAlive() const               { return !m_is_dead; }
    void SetIsDead(bool is_dead=true)  { m_is_dead = is_dead; }
    void KilledByPlayer();
    void KilledWithBullet();
  

Przeznaczenie metod bez problemu odczytamy z ich nazw. Niektóre z nich pojawiły się już wcześniej w implementacji klasy Player. W pliku Types.h należy dodać typ EntityType oraz zmienić nazwę z PlayerState na EntityState (teraz gracz jest także jednostką). Oto uaktualniona zawartość tego pliku:

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
44
45
46
47
48
49
#ifndef __TYPES_H__
#define __TYPES_H__
 
namespace PS {
    enum PlayerState {
        Stand,
        GoLeft,
        GoRight
    };
}
 
namespace DL {
    enum DisplayLayer {
        Foreground = 1,  // przedni plan
        Player = 2,      // plan z graczem
        Background = 3   // tło
    };
}
 
namespace FT {
    enum FieldType {
        None = 0,
        PlatformLeftEnd = 1,
        PlatformMidPart = 2,
        PlatformRightEnd = 3,
 
        COUNT
    };
}
 
// stan jednostki
namespace ES {
    enum EntityState {
        Stand,
        GoLeft,
        GoRight
    };
}
 
// typ jednostki
namespace ET {
    enum EntityType {
        Mush,
        PlayerBullet
    };
}
 
#endif
  

Kolejny zestaw metod dotyczy takich rzeczy jak pobieranie czy zmiana położenia, prędkości oraz przyspieszenia jednostki.

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
44
45
46
47
48
49
50
51
52
53
54
55
    // zarządzenie położeniem 
    double GetX() const { return m_x; }
    double GetY() const { return m_y; }
    virtual double GetNextXPosition(double dt) const { 
        return m_x + GetNextXVelocity(dt) * dt; 
    }
    virtual double GetNextYPosition(double dt) const { 
        return m_y + GetNextYVelocity(dt) * dt; 
    }
    void SetX(double newx)                { m_x = newx; }
    void SetY(double newy)                { m_y = newy; }
    void SetPosition(double x, double y)  { m_x = x; m_y = y;}
 
    // zarządzanie prędkością
    double GetXVelocity() const              { return m_vx; }
    double GetYVelocity() const              { return m_vy; }
    double GetNextXVelocity(double dt) const { 
        return m_vx + m_ax * dt; 
    }
    double GetNextYVelocity(double dt) const { 
        return m_vy + m_ay * dt; 
    }
    double GetDefaultXVelocity() const  { 
        return m_default_velocity_x; 
    }
    double GetDefaultYVelocity() const  { 
        return m_default_velocity_y; 
    }
    void  NegateXVelocity()    { m_vx = -m_vx; }
    void  NegateYVelocity()    { m_vy = -m_vy; }
    void  NegateVelocity()                { 
        NegateXVelocity(); NegateYVelocity(); 
    }
    void  SetXVelocity(double velocity)      { 
        m_vx = velocity; 
    }
    void  SetYVelocity(double velocity)      { 
        m_vy = velocity; 
    }
    void  SetVelocity(double vx, double vy)  {
        m_vx = vx; m_vy = vy; 
    }
 
    // zarządzanie przyspieszeniem
    double GetXAcceleration() const        { return m_ax; }
    double GetYAcceleration() const        { return m_ay; }
    double GetDefaultXAcceleration() const { 
        return m_default_acceleration_x; 
    }
    double GetDefaultYAcceleration() const { 
        return m_default_acceleration_y; 
    }
    void  SetXAcceleration(double accel)   { m_ax = accel; }
    void  SetYAcceleration(double accel)   { m_ay = accel; }
  

Te, które zobaczymy za chwilę są nam bardzo znajome. Wprowadziliśmy w nich jedynie kosmetyczne zmiany, a kilka z nich oznaczyliśmy jako wirtualne, dzięki czemu każda jednostka może określić własny model poruszania się.

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
    // podstawowa zmiana stanu ruchu jednostki
    virtual void GoLeft() { 
        m_vx -= GetDefaultXVelocity(); m_state=ES::GoLeft; 
    }
    virtual void GoRight()   { 
        m_vx += GetDefaultXVelocity(); m_state=ES::GoRight;
    }
    virtual void StopLeft()  { 
        m_vx += GetDefaultXVelocity(); m_state=ES::Stand; 
    }
    virtual void StopRight() { 
        m_vx -= GetDefaultXVelocity(); m_state=ES::Stand; 
    }
    virtual void StopMovement() {m_vx = 0; m_state = ES::Stand;}
    void ForbidGoingLeft()  { m_can_go_left = false; }
    void ForbidGoingRight() { m_can_go_right = false; }
    void Fall()           { m_vy=0.0; m_is_on_ground = false; }
    virtual void SetDefaultMovement()  { 
        m_is_on_ground = false; 
        m_can_go_right = m_can_go_left = true; 
    }
 
    void EntityOnGround() {
        m_is_on_ground = true;
        m_vy = 0;
    }
  

Poznaliśmy już spory kawałek deklaracji klasy Entity. Oto metody, które pozwalają rozeznać się w terenie, czyli sprawdzić jak wygląda mapa nad, pod, przed oraz za graczem. Kilka kolejnych funkcji pozwala nam wygodnie manipulować prostokątem otaczającym jednostkę. Ponieważ ruch postaci będziemy rozpatrywać na każdej osi osobo, to dla każdej z nich chcemy mieć informację na temat AABB. Metodę GetCurrentField omówimy przy okazji prezentacji ciała funkcji IsAnyFieldBelowMe.

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
    // kilka podstawowych informacji 
    // nt. pól mapy wokół jednostki
    bool IsAnyFieldBelowMe(double dt, LevelPtr level) const;
    bool IsAnyFieldAboveMe(double dt, LevelPtr level) const;
    bool IsAnyFieldOnLeft(double dt, LevelPtr level) const;
    bool IsAnyFieldOnRight(double dt, LevelPtr level) const;
    bool DoFieldsEndOnLeft(double dt, LevelPtr level) const;
    bool DoFieldsEndOnRight(double dt, LevelPtr level) const;
 
    // pod argumenty x, y zapisuje numer aktualnego kafla
    void GetCurrentTile(size_t *x, size_t *y) const {
        const size_t v_tiles_count = 
                 Engine::Get().Renderer()->
                       GetVerticalTilesOnScreenCount();
        *y = v_tiles_count 
            - (GetAabb().GetMinY() + GetAabb().GetMaxY()) / 2;
        *x = GetX() + GetBasicAabb().GetMaxX() / 2;
    }
 
    // prostokąt otaczający jednostkę 
    // bez uwzględniania pozycji jednostki
    virtual Aabb GetBasicAabb() const { return Aabb(0,0, 1,1);}
 
    Aabb GetAabb() const { 
        return GetBasicAabb().Move(m_x, m_y, m_x, m_y); 
    }
 
    Aabb GetNextHorizontalAabb(double dt) const {
        return GetBasicAabb().Move(GetNextXPosition(dt), m_y, 
                                   GetNextXPosition(dt), m_y);
    }
 
    Aabb GetNextVerticalAabb(double dt) const {
        return GetBasicAabb().Move(m_x, GetNextYPosition(dt), 
                                   m_x, GetNextYPosition(dt));
    }
 
    Aabb GetNextAabb(double dt) const {
        return GetBasicAabb().Move(
            GetNextXPosition(dt), GetNextYPosition(dt),
            GetNextXPosition(dt), GetNextYPosition(dt) );
    }
  

Pola opisywanej klasy można znaleźć w dołączonym do artykułu archiwum, więc nie będziemy się nimi zajmować. Wiele z nich widzieliśmy wcześniej w klasie Player, a znaczenie pozostałych zostało podane w komentarzach.

0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com