|
Platformówka - jak to się robi?
24.11.2009 - Marcin Milewski
 
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:
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ą
|
|