Następny artykuł - poruszanie postacią
[2]
Opublikowane na Wrocławski Portal Informatyczny (http://informatyka.wroc.pl)
Strona główna > Platformówka - jak to się robi?
Platformówka - jak to się robi?
24.11.2009 - Marcin Milewski
[1]
TrudnośćTrudność
Choć tworzenie gier komputerowych nie jest zadaniem łatwym, to z
całą pewnością można stwierdzić, że jest zajęciem ciekawym. Celem
cyklu, który rozpoczyna się tym artykułem jest stworzenie gry
platformowej w stylu Mario. Poniżej przedstawiamy film z gry, którą
zaraz zaczniemy pisać.
Do zrozumienia tej oraz kolejnych części przydatna będzie znajomość języka programowania
C++ w stopniu podstawowym oraz podstaw użycia biblioteki OpenGL
[3]
(lub trochę czasu na
poeksperymentowanie z przykładami). Będziemy też potrzebować bibliotek SDL
[4]
oraz boost
[5]
. Wszystkie one są dostępne na najpopularniejsze platformy, więc nie powinno być kłopotów
z ich instalacją. Zalecamy wykorzystanie ich najnowszych stabilnych wersji.
Tworzenie okna i wyświetlanie animowanego sprite'a
Na początku naszej zabawy w tworzenie gry dowiemy się, jak stworzyć główne okno gry oraz
jak wyświetlić w nim animowany obrazek. Oto efekt, jaki chcemy uzyskać:
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
1 z 14
2012-12-21 17:01
Jak działają gry?
Ogólnie rzecz biorąc, gra składa się z części logicznej (zarządzanie stanem aplikacji) oraz
wizualnej. Częścią logiczną w każdej aplikacji zarządza pewien mechanizm, na przykład w
systemach operacyjnych są to zdarzenia (naciśnięcie przycisku, wprowadzenie litery, zmiana
rozmiaru okna, itp.). Podstawowym mechanizmem działania gier nie są zdarzenia, lecz pętla
czasu rzeczywistego. Na czym to polega? Otóż gra, od rozpoczęcia aż do zakończenia,
wykonuje w kółko kilka operacji. Oto ogólny schemat:
1
2
3
4
5
6
7
Inicjalizacja okna, gry, sieci, ...
while( true )
- obsłuż wejście (klawiatura, mysz)
- jeżeli należy zakończyć aplikację, to wyjdź z pętli
- wykryj kolizje między obiektami na scenie. Aktualizacja fizyki
- uaktualnij stan obiektów
- narysuj obiekty na ekranie
Tworzenie okna
Po wstępie teoretycznym przejdziemy do pisania aplikacji.
Wykonywanie aplikacji rozpocznie się w pliku main.cpp – zostanie utworzona tam instancja
klasy App, której zadaniem będzie stworzenie okna oraz wykonywanie pętli głównej. Trzy
argumenty, które znajdują się w konstruktorze klasy App to odpowiednio szerokość oraz
wysokość tworzonego okna, a ostatni parametr odpowiada za tryb pełnoekranowy (false
stworzy okno, true stworzy okienko w trybie pełnoekranowym).
1
2
3
4
5
6
#include "App.h"
int
main
(
int
argc,
char
*
argv
[])
{
App app
(
600
,
400
,
false
)
;
app.
Run
()
;
return
0
;
}
Przyjrzyjmy się teraz klasie App.
Pokaż/ukryj kod
1
2
3
4
#include <SDL/SDL.h>
class
App
{
public
:
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
2 z 14
2012-12-21 17:01
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
explicit
App
(
size_t
win_width,
size_t
win_height,
bool
fullscreen_mode
)
:
m_window_width
(
win_width
)
, m_window_height
(
win_height
)
,
m_fullscreen
(
fullscreen_mode
)
{
}
void
Run
()
;
private
:
void
Draw
()
;
// rysowanie
void
Update
(
double
dt
)
;
// aktualizacja
void
Resize
(
size_t
width,
size_t
height
)
;
// zmiana rozmiaru okna
void
ProcessEvents
()
;
// przetwarzanie zdarzeń, które przyszły
private
:
size_t
m_window_width
;
size_t
m_window_height
;
bool
m_fullscreen
;
bool
is_done
;
SDL_Surface
*
m_screen
;
}
;
Poza metodami, których istnienie jest podyktowane wykorzystaniem mechanizmu pętli czasu
rzeczywistego do konstrukcji gry, zauważamy pole m_screen. Jest to struktura
wykorzystywana przez bibliotekę SDL do identyfikacji elementu, po którym można rysować.
SDL udostępnia co prawda mechanizmy do rysowania sprite'ów, jednak my decydujemy się
na wykorzystanie do tego celu biblioteki OpenGL, która daje dużo więcej swobody w sposobie
konstruowania obrazu.
Drugą ciekawą rzeczą w powyższym fragmencie kodu jest argument metody Update. Nazwa
dt to skrót od delta time, czyli znany nam z lekcji fizyki upływ (zmiana) czasu - Δt. Ponieważ
jednak naszym obecnym celem jest stworzenie okna, ten argument jest dla nas chwilowo bez
znaczenia.
Przyjrzyjmy się teraz implementacji metod klasy App. Na początku pliku App.cpp należy
dołączyć dwa pliki:
1
2
#include <cassert>
#include "App.h"
Najpierw omówimy metodę Run, która będzie korzystała z pozostałych metod realizujących
pojedyncze zadania.
Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
void
App
::
Run
()
{
// inicjalizacja okna
SDL_Init
(
SDL_INIT_VIDEO
)
;
Resize
(
m_window_width, m_window_height
)
;
SDL_GL_SetAttribute
(
SDL_GL_DOUBLEBUFFER,
1
)
;
// podwójne buforowanie
// inicjalizacja OpenGL
glClearColor
(
0
,
0
,
0
,
0
)
;
glEnable
(
GL_DEPTH_TEST
)
;
glDepthFunc
(
GL_LEQUAL
)
;
// pętla główna
is_done
=
false
;
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
3 z 14
2012-12-21 17:01
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
size_t
last_ticks
=
SDL_GetTicks
()
;
while
(
!
is_done
)
{
ProcessEvents
()
;
// time update
size_t
ticks
=
SDL_GetTicks
()
;
double
delta_time
=
(
ticks
-
last_ticks
)
/
1000.0
;
last_ticks
=
ticks
;
// update & render
if
(
delta_time
>
0
)
{
Update
(
delta_time
)
;
}
Draw
()
;
}
SDL_Quit
()
;
}
Na początku inicjalizujemy bibliotekę SDL, tak, aby utworzyła nam okno, które będzie
udostępniało podwójne buforowanie. Dalej inicjalizujemy biblitekę OpenGL. Kolejne
wywołania to odpowiednio: ustawienie koloru czyszczenia ekranu (kolor RGB(0,0,0) czyli
czarny), włączenie bufora głębokości i ustawienie trybu jego działania. Należy się słowo
wyjaśniania odnośnie bufora głębokości. Jest to technika, która pozwala na rysowanie
obiektów w dobrej kolejności. Dobrej - to znaczy, że widoczne będą zawsze obiekty, które są
bliżej obserwatora niezależnie od kolejności rysowania. Można nie korzystać z tej techniki,
jednak należy wtedy samemu zadbać, aby obiekty były rysowane od najdalszego planu do
najbliższego.
Pozostała część metody Run to pętla główna. Zapewne nie potrzeba żadnej pomocy w
identyfikacji odpowiedzialnych za kolejne funkcjonalności kawałków kodu, które implementują
główną pętlę gry. Pole is_done jest flagą informującą, czy należy zakończyć wykonywanie
pętli głównej (wartość true) czy ją kontynuować (wartość false). Dalej następuje obsługa
zdarzeń (obsługa wejścia, reakcja okna na akcję użytkownika) oraz wyliczenie czasu, jaki
upłynął od utworzenia ostatniej klatki. W tym właśnie miejscu wyliczamy, ile czasu upłynęło i
przekazujemy tę informację do metody Update. Na koniec pozostaje nam już tylko narysować
wygenerowaną klatkę.
Zastanów się, które z elementów pętli głównej można zamienić miejscami, a dla których nie
ma to sensu.
Pokaż/ukryj odpowiedź
wejście powinno być obsłużone na początku, gdyż może on wpływać na stan gry.
kolizje obiektów należy sprawdzić przed rysowaniem
pomiar czasu powinien znajdować się przed aktualizacją stanu
aktualizacja powinna mieć miejsce przed rysowaniem
Przejdźmy teraz do krótszych metod. Metoda Update na razie jest pusta, ponieważ nie
stworzyliśmy jeszcze żadnych obiektów, które zmieniają swój stan na podstawie upływu
czasu. Metoda Draw czyści bufory koloru oraz głębi, a następnie nakazuje wyświetlić klatkę.
1
2
3
4
5
void
App
::
Draw
()
{
glClear
(
GL_COLOR_BUFFER_BIT
|
GL_DEPTH_BUFFER_BIT
)
;
// tu będziemy coś rysować
// ...
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
4 z 14
2012-12-21 17:01
6
7
8
SDL_GL_SwapBuffers
()
;
}
Metoda przetwarzająca zdarzenia pobiera kolejne zdarzenia z kolejki i albo je obsługuje albo
odrzuca.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void
App
::
ProcessEvents
()
{
if
(
is_done
)
{
// jeżeli mamy zakończyć, to pomijamy obsługę zdarzeń
return
;
}
// przejrzymy kolejne zdarzenia z kolejki
SDL_Event event
;
while
(
SDL_PollEvent
(
&
event
))
{
if
(
event.
type
==
SDL_VIDEORESIZE
)
{
// zmiana rozmiaru okna
Resize
(
event.
resize
.
w
, event.
resize
.
h
)
;
}
else
if
(
event.
type
==
SDL_QUIT
)
{
// zamknięcie okna
is_done
=
true
;
break
;
}
}
}
Została nam ostatnia metoda. Oczywiście nie mniej ważna - wręcz przeciwnie, jedna z
ważniejszych. Zmienia ona rozmiar okna. Wykorzystuje do tego funkcję SDL_SetVideoMode,
która zwraca wskaźnik do odpowiedniej powierzchni, po której można rysować (lub 0 w
przypadku niepowodzenia). W drugiej części kodu znajdują się instrukcje, które informują
OpenGL o tym, jak ma zagospodarować stworzone okno. Na razie chcemy, aby wyświetlał
obraz na całym oknie (funkcja glViewport), do którego będziemy odnosić się używając
współrzędnych [0,1]x[0,1] (punkt (0,0) jest w lewym dolnym rogu, a (1,1) - w prawym górnym)
- funkcja glOrtho. Więcej szczegółów o tych przekształceniach można znaleźć pod adresem
http://glprogramming.com/red/chapter03.html
[6]
. Na koniec przełączamy OpenGL na tryb modelowania, w którym będziemy rysować obiekty.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
App
::
Resize
(
size_t
width,
size_t
height
)
{
m_screen
=
SDL_SetVideoMode
(
width, height,
32
,
SDL_OPENGL
|
SDL_RESIZABLE
|
SDL_HWSURFACE
)
;
assert
(
m_screen
&
"problem z ustawieniem wideo"
)
;
m_window_width
=
width
;
m_window_height
=
height
;
glViewport
(
0
,
0
,
static_cast
<
int
>
(
width
)
,
static_cast
<
int
>
(
height
))
;
glMatrixMode
(
GL_PROJECTION
)
;
glLoadIdentity
()
;
glOrtho
(
0
,
1
,
0
,
1
,
-
1
,
10
)
;
glMatrixMode
(
GL_MODELVIEW
)
;
glLoadIdentity
()
;
}
W ten sposób skończyliśmy pisać kod odpowiedzialny za tworzenie okienka. Wrócimy jeszcze
do niego, gdy będziemy chcieli dodać elementy do gry. W ramach ćwiczeń można pobawić
się naszą aplikacją, zmieniając kilka rzeczy (np. kolor tła, usunięcie czegoś z obsługi zarzeń) i
obserwując, jakie będą konsekwencje.
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
5 z 14
2012-12-21 17:01
Rysujemy trójkąt
Nadszedł czas aby wyświetlić coś więcej niż czarne okienko. Zobaczmy jak łatwe jest
wyświetlenie trójkąta: podajemy współrzędne trzech jego wierzchołków oraz składowe koloru,
a OpenGL zrobi za nas resztę.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
App
::
Draw
()
{
glClear
(
GL_COLOR_BUFFER_BIT
|
GL_DEPTH_BUFFER_BIT
)
;
glLoadIdentity
()
;
glBegin
(
GL_TRIANGLES
)
;
{
glColor3f
(
1
,
1
,
0
)
;
// żółty
glVertex2f
(
1
,
1
)
;
glVertex2f
(
0
,
1
)
;
glVertex2f
(
0
,
0
)
;
// tu można podać kolejne trójkąty
// czyli kolejne wywołania glColor3f, glVertex2f
}
glEnd
()
;
SDL_GL_SwapBuffers
()
;
}
Warto zaznaczyć, że kolejność, w której podane zostały wierzchołki, nie jest przypadkowa -
jest to kierunek przeciwny do ruchu wskazówek zegara (ang. CCW - Counter ClockWise). Oto
efekt działania stworzonego do tej pory kodu:
Jeśli zmienimy rozmiar okna, trójkąt zmienia się razem z nim (kąty nie zostają zachowane). A
jak sprawić, żeby trójkąt skalował się proporcjonalnie?
Jak stworzyć trójkąt, który zawsze będzie wyśrodkowany na ekranie?
Pokaż/ukryj odpowiedź
Problem występuje, gdy zmienimy rozmiar okna tylko w jednym kierunku, lub gdy zmienimy
rozmiar w obu kierunkach, ale nierównomiernie. Zależy nam, aby stosunek szerokości do
wysokości obszaru, po którym rysujemy, był stały. Można zatem zmodyfikować rozmiar tego
obszaru, albo wyliczać współrzędne wierzchołków trójkąta na podstawie wspomnianego
stosunku - w zależności od efektu, który chcemy osiągnąć.
Animacja oparta na sprite'ach
Do tworzenia grafiki w grach 2D najczęściej wykorzystuje się sprite'y, które - odpowiednio
zestawione - tworzą wygląd gry. Sprite (z ang. duszek) to po prostu dwuwymiarowy obrazek
wyświetlany na ekranie. Jest on graficzną reprezentacją podłoża, jak również postaci
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
6 z 14
2012-12-21 17:01
sterowanej przez gracza bądź komputer. Można również w ten sposób przygotować całą
animację czy czcionkę, która będzie wykorzystywana w grze.
Główną zaletą gier opartych na sprite'ach jest ich małe zapotrzebowanie na moc obliczeniową
do wyświetlenia grafiki. Sprite'y przygotowywane są w programach pozwalających tworzyć i
edytować grafikę rastrową, wektorową, czy nawet - trójwymiarową. Następnie zapisywane są
jako sekwencja obrazków. Ze względów (przede wszystkim) wydajnościowych umieszcza się
wiele pojedynczych obrazków w jednym dużym, tworząc w ten sposób tzw. atlas.
Sprite'y nie są oczywiście pozbawione wad, związanych głównie z tym, że używamy grafiki
rastrowej. Tak przygotowane obrazy nie nadają się do powiększania, gdyż prowadzi to do
powstania artefaktów. Kolejnym problemem jest brak możliwości interpolacji, czyli płynnego
przejścia, między dwoma obrazkami (np. klatkami animacji). Te problemy można rozwiązać na
wiele sposobów, jednak nie będziemy się tym zajmować.
W grze przyjmiemy kilka założeń dotyczących sprite'ów:
animacja zawsze mieści się w jednym rzędzie na obrazku
wszystkie klatki danej animacji mają jednakową szerokość i wysokość
wszystkie sprite'y (ogólnie) mieszczą się w jednym pliku
szerokość i wysokość atlasu są potęgami liczby 2
atlas zapisujemy w pliku BMP (nie wymaga zewnętrznej biblioteki do odczytania)
Odczytywanie informacji o sprite'ach
Przejdźmy teraz do sposobu przechowywania informacji o tym, gdzie w atlasie należy szukać
konkretnych sprite'ów. Oto informacje, które musimy znać dla każdej animacji (niektóre
animacje są jednoklatkowe):
położenie lewego dolnego rogu pierwszej klatki w atlasie
szerokość oraz wysokość klatki
liczba klatek w animacji
czas trwania jednej klatki animacji (taki sam dla wszystkich klatek w animacji)
czy po wyświetleniu ostatniej klatki należy przejść znów do pierwszej (zapętlenie), czy
wyświetlać cały czas ostatnią
Na potrzeby gry uprościmy sposób dostarczania danych i nie będziemy ich ładować z pliku,
lecz będą one od razu przechowywane w odpowiednim kontenerze. Struktura przechowująca
informacje o sprite'cie przedstawia się następująco:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct
SpriteConfigData
{
explicit
SpriteConfigData
(
size_t
level,
size_t
frame_count,
double
frame_duration,
double
left,
double
bottom,
double
width,
double
height,
bool
loop
)
:
level
(
level
)
, frame_count
(
frame_count
)
, frame_duration_time
(
frame_duration
left
(
left
)
, bottom
(
bottom
)
, width
(
width
)
, height
(
height
)
, loop
(
loop
)
{
}
size_t
level
;
// plan, na którym będzie rysowany sprite.
// Im bliżej 0, tym bliżej bliżej obserwatora (bliższy plan)
size_t
frame_count
;
// liczba klatek w animacji
double
frame_duration_time
;
// czas trwania klatki
double
left
;
// położenie w poziomie pierwszej klatki animacji w obrazku (w px)
double
bottom
;
// położenie w pionie pierwszej klatki animacji w obrazku (w px)
double
width
;
// szerokość klatki w pikselach
double
height
;
// wysokość klatki w pikselach
bool
loop
;
// czy animacja ma być zapętlona?
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
7 z 14
2012-12-21 17:01
18
}
;
Potrzebujemy jeszcze klasy, która będzie zajmowała się ładowaniem tych danych oraz
ewentualnym informowaniem, że żądane dane nie są dostępne. Do gromadzenia danych
wykorzystamy standardowy kontener map. Oto definicja klasy SpriteConfig:
1
2
3
4
5
6
7
8
9
10
11
class
SpriteConfig
{
public
:
explicit
SpriteConfig
()
;
SpriteConfigData Get
(
const
std
::
string
&
name
)
const
;
bool
Contains
(
const
std
::
string
&
name
)
const
;
private
:
std
::
map
m_data
;
void
Insert
(
const
std
::
string
&
name,
const
SpriteConfigData
&
data
)
;
}
;
typedef
boost
::
shared_ptr
SpriteConfigPtr
;
Wyjaśnienia wymaga na pewno ostatnia linijka na powyższym listingu. Zdefiniowaliśmy w niej
shared pointer dla klasy SpriteConfig, aby skrócić zapis. W grze będziemy wykorzystywać
mechanizm shared pointerów w miejscach, gdzie można by użyć zwykłych wskaźników.
Unikniemy w ten sposób niepotrzebnych problemów ze zwalnianiem pamięci, gdyż te
wskaźniki zwolnią się same, gdy już nie będą potrzebne. Implementacja metod powyższej
klasy jest bardzo prosta:
Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SpriteConfig
::
SpriteConfig
()
{
// przykładowe informacje o sprite'ach
Insert
(
"liczby"
, SpriteConfigData
(
5
,
4
,
1
,
0
,
38
,
50
,
38
,
false
))
;
Insert
(
"liczby-loop"
, SpriteConfigData
(
5
,
4
,
1
,
0
,
38
,
50
,
38
,
true
))
;
Insert
(
"litery"
, SpriteConfigData
(
5
,
3
,
.5
,
0
,
77
,
50
,
38
,
true
))
;
}
SpriteConfigData SpriteConfig
::
Get
(
const
std
::
string
&
name
)
const
{
if
(
Contains
(
name
))
return
m_data.
find
(
name
)
-
>
second
;
throw
(
"Config not found: "
+
name
)
;
}
bool
SpriteConfig
::
Contains
(
const
std
::
string
&
name
)
const
{
return
(
m_data.
find
(
name
)
!
=
m_data.
end
())
;
}
void
SpriteConfig
::
Insert
(
const
std
::
string
&
name,
const
SpriteConfigData
&
data
m_data.
insert
(
std
::
make_pair
(
name, data
))
;
}
Obiekty tworzone w konstruktorze klasy SpriteConfig definiują animacje na poniższym
obrazku. Punkt (0,0) umieszony jest w lewym górnym rogu, a punkt (1,1) - w prawym dolnym.
Szerokość i wysokość sprite'a określamy w pikselach.
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
8 z 14
2012-12-21 17:01
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
#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
)
;
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
9 z 14
2012-12-21 17:01
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
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)
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
10 z 14
2012-12-21 17:01
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
#include "SpriteConfig.h"
#include "Renderer.h"
class
Engine
{
public
:
// zwraca instancję klasy Engine - jedyną
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
11 z 14
2012-12-21 17:01
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
void
App
::
Update
(
double
dt
)
{
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
12 z 14
2012-12-21 17:01
Następny artykuł - poruszanie postacią
[2]
3
4
5
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
[7]
. Dostępna jest też wersja z projektem Visual C++ 2008 +
plik exe
[8]
. 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
[9]
.
Masz pytanie, uwagę? Zauważyłeś błąd? Powiedz o tym na forum
[10]
.
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.
Adres źródła: http://informatyka.wroc.pl/node/387
Odnośniki:
[1] http://marcindev.blogspot.com/
[2] http://informatyka.wroc.pl/node/422
[3] http://www.opengl.org/
[4] http://www.libsdl.org/download-1.2.php
[5] http://www.boost.org/
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
13 z 14
2012-12-21 17:01
[6] http://glprogramming.com/red/chapter03.html
[7] http://informatyka.wroc.pl/upload/mmi/platf/01_final.zip
[8] http://informatyka.wroc.pl/upload/mmi/platf/01_final_vc.zip
[9] http://informatyka.wroc.pl/forum/viewtopic.php?p=1248#p1248
[10] http://informatyka.wroc.pl/forum/viewtopic.php?f=55&t=358
Platformówka - jak to się robi?
http://informatyka.wroc.pl/print/387
14 z 14
2012-12-21 17:01