Sprawmy by była piękna

11.11.2010 - Łukasz Milewski
TrudnośćTrudność

Wchodzisz do sklepu chcąc kupić grę komputerową. Przez chwilę oglądasz screeny i oceniasz grafikę. Ładna, więc warto kupić! Tak samo jak my, gry mają tylko jedną szansę aby zrobić pierwsze wrażenie. Dobrze zrobiona grafika może tylko pomóc naszej grze. Spójrzmy jak łatwo możemy zmienić szatę graficzną naszej platformówki.

Poprzedni artykuł - Efektowne przejścia między stanami Następny artykuł - Edytor poziomów cz. 1

Nowe grafiki

Skupimy się jedynie na stronie programistycznej dołączania nowej grafiki do gry i wykorzystamy teksturę przygotowaną w serii artykułów Grafika do platformówki. Zobaczmy jak prezentuje się nowa grafika w porównaniu do starej.

Co nowego?

Zobaczmy jakie nowe elementy zostały przygotowane. Spójrzmy na najnowszą wersję tekstury
Od razu widzimy, że grafika nie tylko uległa zmianie. wzrosła również liczba elementów. Poprzednio mieliśmy dostępne trzy rodzaje muru:
  1. lewy koniec platformy
  2. środkową część platformy
  3. prawy koniec platformy
Obecnie możemy wykorzystać
  1. lewy górny róg platformy
  2. lewą krawędź platformy
  3. prawy górny róg platformy
  4. prawą krawędź platformy
  5. środkową górną część platformy
  6. środkową wewnętrzną część platformy
  7. dwa kafle stanowiące całą platformę
Zmianie uległ również znak końca poziomu - od teraz będzie animowany. Ilość klatek animacji gracza wzrosła z szesnastu do dwudziestu pięciu, a ta sama zmiana dla przeciwników to wzrost z czterech do szesnastu. Również czcionka zyskała nowy wygląd, obecnie jest czytelniejsza. Ekran wyboru poziomu także doczekał się odnowienia. Pojawiły się grafiki drogi, gracza oraz animowanego portalu. Na zamieszczonej grafice jest kilka elementów, których nie wykorzystamy. Został dodany również przykładowy fragment mapy złożony z nowych elementów. Widać na nim ciekawy efekt, który zastosujemy - przyciemnione kafle. Dzięki tej technice będziemy mogli złożyć tło z tych samych kafli, z których jest zbudowany pierwszy plan.

Nowe kafle platform

Zmiany zaczniemy od wprowadzenia nowych kafli platform. Poprzednio w pliku Game.cpp był taki kod:
1
2
3
4
5
6
7
8
9
    m_level_view.StoreSprite(FT::PlatformLeftEnd,  
        SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("platform_left"))));
    m_level_view.StoreSprite(FT::PlatformMidPart,
        SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("platform_mid"))));
    m_level_view.StoreSprite(FT::PlatformRightEnd,
        SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("platform_right"))));
    m_level_view.StoreSprite(FT::EndOfLevel,
        SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("end_of_level"))));
  
Zamiast trzech sprite'ów mapy oraz jednego spirte'a końca poziomu wczytamy osiem nowych sprite'ów mapy oraz nowy sprite końca poziomu. Sprite'y mapy wczytamy dwukrotnie (aby móc przyciemnić jeden zestaw).
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
    m_level_view.StoreSprite(FT::PlatformTopLeft,
        SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatformTopLeft"))));
    m_level_view.StoreSprite(FT::PlatformLeft,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatformLeft"))));
    m_level_view.StoreSprite(FT::PlatfromMid,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatfromMid"))));
    m_level_view.StoreSprite(FT::PlatformTop,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatformTop"))));
    m_level_view.StoreSprite(FT::PlatformLeftTopRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatformLeftTopRight"))));
    m_level_view.StoreSprite(FT::PlatformLeftRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatformLeftRight"))));
    m_level_view.StoreSprite(FT::PlatformTopRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatformTopRight"))));
    m_level_view.StoreSprite(FT::PlatformRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("PlatformRight"))));
 
    m_level_view.StoreSprite(FT::EndOfLevel,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("EndOfLevel"))));
 
    m_level_view.StoreSprite(FT::NcPlatformTopLeft,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatformTopLeft"))));
    m_level_view.StoreSprite(FT::NcPlatformLeft,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatformLeft"))));
    m_level_view.StoreSprite(FT::NcPlatfromMid,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatfromMid"))));
    m_level_view.StoreSprite(FT::NcPlatformTop,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatformTop"))));
    m_level_view.StoreSprite(FT::NcPlatformLeftTopRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatformLeftTopRight"))));
    m_level_view.StoreSprite(FT::NcPlatformLeftRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatformLeftRight"))));
    m_level_view.StoreSprite(FT::NcPlatformTopRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatformTopRight"))));
    m_level_view.StoreSprite(FT::NcPlatformRight,
         SpritePtr(new Sprite(engine.GetSpriteConfig()->Get("NcPlatformRight"))));
  
Wprowadziliśmy właśnie nowe nazwy wyświetlanych pól (np. FT::NcPlatformRight). Sprite'y z przedrostkiem Nc w nazwie będą przyciemnione i nie będą brane pod uwagę podczas wykrywania kolizji. Poprzednio plik Types.h zawierał następującą definicję
1
2
3
4
5
6
7
8
9
10
11
12
13
namespace FT {
    enum FieldType {
        None = 0,
        PlatformLeftEnd = 1,
        PlatformMidPart = 2,
        PlatformRightEnd = 3,
 
        EndOfLevel = 4,
 
        COUNT
    };
}
  
Zmienimy ją, tak aby obejmowała wszystkie nowe typy kafli. Dodatkowo podzielimy definicje na kolidujące i niekolidujące. To znacznie uprości wykrywanie kolizji z mapą. Nowy plik Types.h zawiera następującą definicję FT: namespace FT {
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
    enum FieldType {
        None = 0,
        EndOfLevel = 1,
 
        COLLIDING_START = 10000,
 
        PlatformTopLeft, // 10001
        PlatformLeft, // 10002
        PlatfromMid, // 10003
        PlatformTop, // 10004
        PlatformLeftTopRight, // 10005
        PlatformLeftRight, // 10006
        PlatformTopRight, // 10007
        PlatformRight, // 10008
 
        COLLIDING_END,
 
        NON_COLLIDING_START = 20000,
 
        NcPlatformTopLeft,
        NcPlatformLeft,
        NcPlatfromMid,
        NcPlatformTop,
        NcPlatformLeftTopRight,
        NcPlatformLeftRight,
        NcPlatformTopRight,
        NcPlatformRight,
        NcCandleWithBg,
        NcCandleNoBg,
        NcCrackWithBg,
        NcCrackNoBg,
 
        NON_COLLIDING_END
    };
}
  
Musimy jeszcze poprawić definicję sprite'ów tak, aby były obrazki były wczytywane z nowego pliku. W tym celu zmodyfikujemy plik SpriteConfig.cpp. Jego poprzednia wersja zawierała następujące definicje
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
  SpriteConfig::SpriteConfig() {
    Insert("player_right",
         SpriteConfigData(DL::Player, 4, 0.1, 0, 4 * 32, 32, 32, true));
    Insert("player_left",
          SpriteConfigData(DL::Player, 4, 0.1, 0, 5 * 32, 32, 32, true));
    Insert("player_stop",
          SpriteConfigData(DL::Player, 8, 0.1, 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("end_of_level",
           SpriteConfigData(DL::Foreground, 1, 1, 32, 2*32, 32, 32, true));
 
    Insert("mush_right",
         SpriteConfigData(DL::Entity, 4, 0.3,  0, 11 * 32, 32, 32, true));
    Insert("mush_left",
          SpriteConfigData(DL::Entity, 4, 0.3,  0, 11 * 32, 32, 32, true));
    Insert("mush_stop",
          SpriteConfigData(DL::Entity, 1, 0.3,  0, 12 * 32, 32, 32, true));
 
    Insert("player_bullet",
          SpriteConfigData(DL::Entity, 4, 0.3,  5*32, 11*32, 32, 32, true));
 
    Insert("twinshot_upgrade",
         SpriteConfigData(DL::Entity, 2, 0.3,  0*32, 13*32, 32, 32, true));
  }
  
Nowa definicja jest analogiczna do poprzedniej. Zmieniamy jedynie liczby odpowiedzialne za położenie sprite'ów w teksturze. Wywołań metody Insert jest więcej, bo chcemy załadować wszystkie nowe sprite'y. Ciekawą zmianą jest dodanie nowego parametru do konstruktora SpriteConfigData. Ten ostatni parametr typu bool to informacja czy dany sprite ma być przyciemniony. Jeżeli ustawimy go na true to sprite będzie ustawiany jako tło (tak jak widzieliśmy na teksturze z nowymi grafikami).
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
56
57
  SpriteConfig::SpriteConfig() {
 
    Insert("PlatformTopLeft"      , SpriteConfigData(DL::Foreground,
         1, 1,   0*32, 1*32, 32, 32, false, false));
    Insert("PlatformLeft"         ,
         SpriteConfigData(DL::Foreground, 1, 1,   1*32, 1*32, 32, 32, false, false));
    Insert("PlatfromMid"          ,
         SpriteConfigData(DL::Foreground, 1, 1,   0*32, 2*32, 32, 32, false, false));
    Insert("PlatformTop"          ,
         SpriteConfigData(DL::Foreground, 1, 1,   1*32, 2*32, 32, 32, false, false));
    Insert("PlatformLeftTopRight" ,
         SpriteConfigData(DL::Foreground, 1, 1,   2*32, 2*32, 32, 32, false, false));
    Insert("PlatformLeftRight"    ,
         SpriteConfigData(DL::Foreground, 1, 1,   3*32, 2*32, 32, 32, false, false));
    Insert("PlatformTopRight"     ,
         SpriteConfigData(DL::Foreground, 1, 1,   0*32, 3*32, 32, 32, false, false));
    Insert("PlatformRight"        ,
         SpriteConfigData(DL::Foreground, 1, 1,   1*32, 3*32, 32, 32, false, false));
    Insert("EndOfLevel"           ,
         SpriteConfigData(DL::Foreground, 4, 0.1, 4*32, 2*32, 32, 32, true, false));
 
    Insert("NcPlatformTopLeft"      ,
         SpriteConfigData(DL::Background, 1, 1,   0*32, 1*32, 32, 32, false, true));
    Insert("NcPlatformLeft"         ,
         SpriteConfigData(DL::Background, 1, 1,   1*32, 1*32, 32, 32, false, true));
    Insert("NcPlatfromMid"          ,
         SpriteConfigData(DL::Background, 1, 1,   0*32, 2*32, 32, 32, false, true));
    Insert("NcPlatformTop"          ,
         SpriteConfigData(DL::Background, 1, 1,   1*32, 2*32, 32, 32, false, true));
    Insert("NcPlatformLeftTopRight" ,
         SpriteConfigData(DL::Background, 1, 1,   2*32, 2*32, 32, 32, false, true));
    Insert("NcPlatformLeftRight"    ,
         SpriteConfigData(DL::Background, 1, 1,   3*32, 2*32, 32, 32, false, true));
    Insert("NcPlatformTopRight"     ,
         SpriteConfigData(DL::Background, 1, 1,   0*32, 3*32, 32, 32, false, true));
    Insert("NcPlatformRight"        ,
         SpriteConfigData(DL::Background, 1, 1,   1*32, 3*32, 32, 32, false, true));
 
    Insert("mush_right",
         SpriteConfigData(DL::Entity, 4, 0.3,  0, 12 * 32, 32, 32, true, false));
    Insert("mush_left",
          SpriteConfigData(DL::Entity, 4, 0.3,  0, 13 * 32, 32, 32, true, false));
    Insert("mush_stop",
          SpriteConfigData(DL::Entity, 4, 0.3,  0, 14 * 32, 32, 32, true, false));
 
    Insert("player_right",
         SpriteConfigData(DL::Player, 4, 0.1, 0, 4 * 32, 32, 32, true, false));
    Insert("player_left",
          SpriteConfigData(DL::Player, 4, 0.1, 0, 5 * 32, 32, 32, true, false));
    Insert("player_stop",
          SpriteConfigData(DL::Player, 8, 0.1, 0, 6 * 32, 32, 32, true, false));
    Insert("player_bullet",
          SpriteConfigData(DL::Entity, 1, 0.3,  6*32, 13*32, 32, 32, true, false));
    Insert("twinshot_upgrade",
         SpriteConfigData(DL::Entity, 4, 0.1,  6*32, 15*32, 32, 32, true, false));
  }
  
Nowy konstruktor SpriteConfigData (plik Sprite.h) powinien ustawiać nowe pole (m_dark) tej klasy na wartość właśnie dodanego parametru. Aby pole m_dark było brane pod uwagę przy rysowaniu sprite'a musimy zmodyfikować renderer. Zatem zmieniamy w pliku Renderer.cpp metodę
1
2
3
4
    void Renderer::DrawSprite(double tex_x, double tex_y, double tex_w, double tex_h,
                          double pos_x, double pos_y, double width, double height, 
                          DL::DisplayLayer layer) {
  
na
1
2
3
4
5
    void Renderer::DrawSprite(double tex_x, double tex_y, double tex_w, double tex_h,
                          double pos_x, double pos_y, double width, double height, 
                          DL::DisplayLayer layer,
                          double brightness) {
  
Powinniśmy również zmienić odpowiednio plik Renderer.h Nowy parametr - brightness określa jak bardzo jasny jest sprite (jego wartość to liczba z przedziału [0..1]). W ciele metody DrawSprite zmieniamy glColor3f(1,1,1); na glColor3f(brightness, brightness, brightness); Jeżeli dla nowego parametru damy wartość domyślną 1.0 to system będzie się zachowywał identycznie jak poprzednio. Aby rysować przyciemnione sprite'y wystarczy w zmodyfikować metodę Sprite::DrawCurrentFrame tak, aby w przypadku ciemnego sprite'a używała nowego parametru
1
2
3
4
5
6
7
8
9
10
11
12
        if (m_dark) {
            Engine::Get().GetRenderer()->DrawSprite(
                m_data.left + m_data.width * m_current_frame, m_data.bottom,
                m_data.width, m_data.height, x, y, width, height, m_data.layer,
                0.15);
        }
        else {
            Engine::Get().GetRenderer()->DrawSprite(
                m_data.left + m_data.width * m_current_frame, m_data.bottom,
                m_data.width, m_data.height, x, y, width, height, m_data.layer);
        }
  

Nowa czcionka

Do działania systemu czcionek nie wprowadzamy żadnych modyfikacji. Mimo to musimy zmodyfikować plik Text.cpp gdyż odpowiednie grafiki zostały przesunięte. Powinniśmy wprowadzić następujące zmiany:
  • zamienić int tex_y = 7*32; na int tex_y = 8*32;
  • zamienić int tex_y = (8+letter_row) * 32; na int tex_y = (9+letter_row) * 32;
  • zamienić tex_y = 320; na tex_y = 352;

Animowane kafle

Kolejną zmianą jaką wprowadzamy razem z nową grafiką są animowane kafle. My mamy tylko jeden taki kafel - portal. Tak, znak końca poziomu traktujemy jak kawałek mapy. Jedyne co musimy zrobić to wprowadzić odświeżanie stanu każdego z wyświetlanych kafli w każdym obiegu pętli głównej gry. Musimy zatem zmodyfikować klasę SpriteGrid. Ponieważ wprowadziliśmy nieciągłe numerowanie kafli (zmiana w pliku Types.h) to zmieniamy teraz w pliku SpriteGrid.h typ pola m_sprites z
1
2
    std::vector<SpritePtr> m_sprites;
  
na
1
2
    std::map<FT::FieldType, SpritePtr> m_sprites;
  
Dodatkowo jeżeli odpowiedni sprite nie istnieje w trakcie rysowania to chcemy ustawić tam pusty sprite. Zamieniamy definicję metody SetLevel
1
2
3
4
5
6
  void SpriteGrid::SetLevel(const LevelPtr lvl, double dx) {
   //...
                  SetSprite(x, y, m_sprites.at(ft));
   //...
  }
  
na
1
2
3
4
5
6
7
8
9
10
11
12
  void SpriteGrid::SetLevel(const LevelPtr lvl, double dx) {
   //...
                  std::map<FT::FieldType, SpritePtr>::iterator it = m_sprites.find(ft);
                  if (it != m_sprites.end()) {
                      SetSprite(x, y, it->second);
                  }
                  else {
                      SetSprite(x, y, SpritePtr());
                  }
   //...
  }
  
Musimy również dostosować metodę StoreSprite do nowego typu danych m_sprites. Zmieniamy
1
2
3
4
5
  void SpriteGrid::StoreSprite(FT::FieldType ft, SpritePtr sp) {
      if (m_sprites.size() <= static_cast<size_t>(ft)) m_sprites.resize(ft + 1);
      m_sprites.at(ft) = sp;
  }
  
na
1
2
3
4
5
  SpriteGrid.cpp~NEW
  void SpriteGrid::StoreSprite(FT::FieldType ft, SpritePtr sp) {
      m_sprites.insert(std::make_pair(ft, sp));
  }
  
Została jedynie najważniejsza zmiana - dodajemy do SpriteGrid metodę Update:
1
2
3
4
5
6
7
8
9
10
11
12
  void SpriteGrid::Update(double dt) {
      for (size_t y = 0; y < m_grid.size(); ++y) {
          const std::vector<SpritePtr>& row = m_grid.at(y);
          for (size_t x = 0; x < row.size(); ++x) {
              const SpritePtr& sprite = row.at(x);
              if (sprite) {
                  sprite->Update(dt);
              }
          }
      }
  }
  
Dzięki temu wszystkie widoczne sprite'y będą zawsze aktualizowane. Pozwala to animować sprite portalu ale również wprowadzić w przyszłości kafle, które zmieniają swój wygląd w czasie gry. Pozostaje jedynie wywołać tę, nową metodę w Game::Update w pliku Game.cpp. Tuż przed
1
2
    return !IsDone(); // było tylko to
  
dopisujemy linijki
1
2
3
    // zaaktualizuj stan mapy kaflowej (np. animację kafli)
    m_level_view.Update(dt);
  

Nowy ekran wyboru poziomu

Aby gra wyglądała spójnie zmieniamy także LevelChoicesScreen.cpp. Podobnie jak poprzednio zmieniliśmy definicję sprite'ów w Game.cpp, tak teraz robimy to LevelChoicesScreen.cpp. Na nowo ustalamy pozycje odpowiednich animacji w pliku z grafiką. Zamieniamy
1
2
3
4
5
6
7
8
9
    const SpriteConfigData horizontal_road_data(DL::Foreground,
         1, 1, 0 * 32, 14 * 32, 32, 32, false);
    const SpriteConfigData vertical_road_data(DL::Foreground,
         1, 1,  1 * 32, 14 * 32, 32, 32, false);
    const SpriteConfigData entry_enabled_data(DL::Foreground,
         1, 1,  6 * 32, 14 * 32, 32, 32, false);
    const SpriteConfigData face_data(DL::Foreground,
         8, .1,  8 * 32, 14 * 32, 32, 32, true);
  
na
1
2
3
4
5
6
7
8
9
    const SpriteConfigData horizontal_road_data(DL::Foreground,
         1, 1, 0 * 32, 17 * 32, 32, 32, false, false);
    const SpriteConfigData vertical_road_data(DL::Foreground,
         1, 1,  1 * 32, 17 * 32, 32, 32, false, false);
    const SpriteConfigData entry_enabled_data(DL::Foreground,
         4, .1,  4 * 32, 17 * 32, 32, 32, true, false);
    const SpriteConfigData face_data(DL::Foreground,
         4, .1,  0 * 32, 23 * 32, 32, 32, true, false);
  

Poprawiamy dane

Oprócz kodu powinniśmy zmienić również pliki z danymi gry - pliki poziomów. Po zmianie definicji typów pól nie możemy już wykorzystywać liczb 1,2,3,itd jako identyfikatorów kafli. Obecnie kolidują kafle na mapie mają numery od 10000 do 20000. Dlatego musimy odpowiednio zmodyfikować wszystkie pliki w katalogu data, które mają roszerzenie *.lvl.

Pobierz nowy kod źródłowy

Ćwiczenia

  1. Na teksturze dołączonej do gry jest kilka elementów, których nie wprowadziliśmy do gry. Między nimi jest również kolumna. Dodaj obsługę kolumny do gry. Gracz nie powinien kolidować z kolumną. Powinien móc jednak przejść za nią
0
Twoja ocena: Brak

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com