Platformówka - jak to się robi?

24.11.2009 - Marcin Milewski
TrudnośćTrudność

Klasy niezbędne do wyświetlania sprite’ów

Zanim przejdziemy do samego zarządzania spritem, stworzymy kod, który wczyta nasz atlas z dysku oraz będzie potrafił wyświetlić jego kawałek w wyznaczonym miejscu na ekranie. W ten sposób dochodzimy do klasy Renderer. Oto jej implementacja:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <SDL/SDL.h>
#include <SDL/SDL_opengl.h>
#include <iostream>
#include "Renderer.h"
 
void Renderer::LoadTexture(const std::string & filename) {
  // załaduj bitmapę z pliku
  SDL_Surface* surface = SDL_LoadBMP(filename.c_str());
  if (!surface) {
    std::cerr << "Ładowanie pliku " + filename + " FAILED: " 
                 + SDL_GetError() + "\n";
    exit(1);
  }
 
  // sprawdź wymiary - czy są potęgą 2
  const int width = surface->w;
  const int height = surface->h;
  if (((width & (width - 1)) != 0) || ((height & (height - 1)) != 0)) {
    std::cerr << "Obrazek " + filename 
                 + " ma nieprawidłowe wymiary (powinny być potęgą 2): "
              << width << "x" << height << "\n";
    exit(1);
  }
 
  GLenum format;
  switch (surface->format->BytesPerPixel) {
     case 3:  format = GL_BGR;   break;
     case 4:  format = GL_BGRA;  break;
     default:  std::cerr << "Nieznany format pliku " + filename + "\n";
               exit(1);
  }
 
  glGenTextures(1, &m_texture);
  glBindTexture(GL_TEXTURE_2D, m_texture);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
  glTexImage2D(GL_TEXTURE_2D, 0, surface->format->BytesPerPixel,
               width, height, 0, format, GL_UNSIGNED_BYTE, surface->pixels);
  if (surface) {
    SDL_FreeSurface(surface);
  }
}
 
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, 
                          size_t level) {
  const double texture_w = 256.0;    // szerokość atlasu
  const double texture_h = 128.0;    // wysokość atlasu
 
  const double left = tex_x / texture_w;
  const double right = left + tex_w / texture_w;
  const double bottom = tex_y / texture_h;
  const double top = bottom - tex_h / texture_h;
  /* Obrazek ładowany jest do góry nogami, więc punkt (0,0) 
   * jest w lewym górnym rogu tekstury.
   * Stąd wynika, że w powyższym wzorze top jest poniżej bottom
   */
 
  glPushMatrix();  {
    glTranslatef(0, 0, -static_cast<int>(level));
    glBegin(GL_QUADS);  {
      glColor3f(1, 1, 1);
      glTexCoord2f(right, top);     glVertex2f(pos_x+width, pos_y+height);
      glTexCoord2f(left, top);      glVertex2f(pos_x,       pos_y+height);
      glTexCoord2f(left, bottom);   glVertex2f(pos_x,       pos_y);
      glTexCoord2f(right, bottom);  glVertex2f(pos_x+width, pos_y);
    }
    glEnd();
  }
  glPopMatrix();
}

Powyższy schemat ładowania tekstury i wyświetlania jego fragmentu jest typowy dla OpenGL. Oto elementy, na które należy zwrócić uwagę:

  • rozmiary tekstury muszą być potęgą liczby 2;
  • początek układy współrzędnych tekstury (punkt (0,0)) jest umieszczony w lewym dolnym rogu - punkt (1,1) jest w prawym górnym rogu;
  • pliki BMP mają zamienione składowe R i B, dlatego jako format ustawiamy GL_BGR lub GL_BGRA (w zależności od tego czy jest to plik 24 czy 32 bitowy);
  • pliki BMP są zapisane do góry nogami;
  • do tekstury (czyli załadowanego do OpenGL obrazka) mamy dostęp we współrzędnych [0,1]x[0,1] - stąd wynikają dzielenia przy wyliczaniu stałych left, right, bottom oraz top.

Jesteśmy gotowi na zapoznanie się z klasą Sprite. Schemat jej działania jest bardzo prosty:

  • tworzenie instancji: wczytaj informacje nt. sprite'a z obiektu SpriteConfigData;
  • aktualizacja sprite'a: sprawdź czy należy zmienić aktualną klatkę. Jeżeli, tak to zmień;
  • rysowanie: przekaż odpowiednie argumenty do klasy Renderer. Położenie aktualnej klatki wyznaczamy jako (położenie zerowej klatki) + (szerokość klatki) * (numer aktualnej klatki)

Definicja klasy Sprite:

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
#include "Engine.h"
#include "Sprite.h"
 
Sprite::Sprite(const SpriteConfigData& data) :
  m_data(data), m_current_frame(0), m_current_frame_duration(0.0) { }
 
void Sprite::SetCurrentFrame(size_t frame_num) {
  m_current_frame = frame_num;
  m_current_frame_duration = 0.0;  // początek tej klatki
}
 
void Sprite::Update(double dt) {
  // klatka jest wyświetlana o dt dłużej
  m_current_frame_duration += dt;
 
  // przejdź do następnej klatki jeżeli trzeba
  if (m_current_frame_duration >= m_data.frame_duration_time) {
    m_current_frame++;
    m_current_frame_duration -= m_data.frame_duration_time;
  }
 
  // sprawdź czy nastąpił koniec animacji 
  // - przejdź do klatki 0. lub ostatniej
  if (m_current_frame >= m_data.frame_count) {
    if (m_data.loop) {
      m_current_frame = 0;
    } else {
      m_current_frame = m_data.frame_count - 1;
    }
  }
}
 
void Sprite::DrawCurrentFrame(double x, double y, 
                              double width, double height) {
  Engine::Get().Renderer()->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.level);
}

Wykorzystana klasa Engine jest prostym singletonem, który na razie umożliwia nam dostęp do dwóch klas: Renderer oraz SpriteConfig. Jej zadaniem jest przechowywanie wskaźników na instancje odpowiednich klas oraz udostępnianie ich na żądanie.

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
#include "SpriteConfig.h"
#include "Renderer.h"
 
class Engine {
public:
  // zwraca instancję klasy Engine - jedyną
  static Engine& Get() {
    static Engine engine;
    return engine;
  }
 
  // inicjalizacja klasy Engine - utworzenie instancji odpowiednich klas
  void Load() {
    m_spriteConfig.reset(new SpriteConfig::SpriteConfig());
    m_renderer.reset(new Renderer::Renderer());
  }
 
  SpriteConfigPtr SpriteConfig() {  return m_spriteConfig;  }
  RendererPtr Renderer() {  return m_renderer;  }
 
private:
  SpriteConfigPtr m_spriteConfig;
  RendererPtr m_renderer;
};

Wyświetlanie sprite'ów

Teraz czas wykorzystać klasy, które do tej pory stworzyliśmy. Przykładowe dane dotyczące sprite'ów są już w klasie SpriteConfig. Zatem to, czego nam brakuje, to dodanie ich do klasy App tak, aby były aktualizowane i wyświetlane. Oto kawałek metody App::Run, który uległ zmianie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  // inicjalizacja OpenGL
  glClearColor(0, 0, 0, 0);
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LEQUAL);
  glEnable(GL_TEXTURE_2D);
 
  const std::string test_sprite_filename = "data/counter.bmp";  // nasz atlas
  Engine& engine = Engine::Get();
  engine.Load();
  engine.Renderer()->LoadTexture(test_sprite_filename);
  m_litery.reset(new Sprite(engine.SpriteConfig()->Get("litery")));
  m_liczby.reset(new Sprite(engine.SpriteConfig()->Get("liczby")));
  m_liczby_loop.reset(new Sprite(engine.SpriteConfig()->Get("liczby-loop")));
 
  // pętla główna
  is_done = false;
  size_t last_ticks = SDL_GetTicks();
  while (!is_done) {

m_litery, m_liczby i m_liczby_loop to oczywiście prywatne pola klasy App, które reprezentują kilka przykładowych sprite'ów:

1
2
3
  SpritePtr m_litery;
  SpritePtr m_liczby;
  SpritePtr m_liczby_loop;

Nietrudno domyślić się jak będzie wyglądała metoda App::Update:

1
2
3
4
5
void App::Update(double dt) {
  m_litery->Update(dt);
  m_liczby->Update(dt);
  m_liczby_loop->Update(dt);
}

Pozostało już tylko narysowanie sprite'ów na ekranie:

1
2
3
4
5
6
7
8
void App::Draw() {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity();
  m_litery->DrawCurrentFrame(0, 0, .5, .5); // x, y, width, height
  m_liczby->DrawCurrentFrame(.5, 0, .3, .3);
  m_liczby_loop->DrawCurrentFrame(.5, .5, .4, .4);
  SDL_GL_SwapBuffers();
}

Oto kawałek animacji, którą stworzyliśmy w tym artykule:

Trójkąt w oknie

Pliki z kodem źródłowym są tutaj. Dostępna jest też wersja z projektem Visual C++ 2008 + plik exe. Jeżeli próbujesz skompilować źródła i uruchomić grę w systemie Windows używając środowiska Dev-C++, to być może pomocne będą te wskazówki.

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

Zadanka dla dociekliwych :)

  • Dla uproszczenia dane dotyczące konfiguracji sprite'ów są przechowywane w programie (w konstruktorze klasy SpriteConfig). Zmień tę klasę tak, aby te dane wczytywane były z pliku.
  • Zastanów się jak można rozwiązać problem mieszczenia się animacji w jednym rzędzie w pliku, tzn. jak należy wyliczać położenia klatki animacji, wiedząc, ile klatek mieści się w rzędzie oraz znając numer klatki do wyświetlenia. Wskazówka: Wykorzystaj operacje dzielenia całkowitoliczbowego oraz reszty z dzielenia.

  Następny artykuł - poruszanie postacią

4.57143
Twoja ocena: Brak Ocena: 4.6 (7 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com