Gra 2D, część 1 Platformówka jak to się robi

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

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

background image

[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


Wyszukiwarka

Podobne podstrony:
Jak to się robi poza Polską
Pozycjonowanie i optymalizacja stron WWW Jak to sie robi
oto jak to sie robi!! G
Jak to się robi buty
Jak to się robi w Niemczech - Podatek kościelny w działaniu, Katechistan - kraju Polan powstań z kol
oto jak to sie robi 3
Jak to się robi - skarpety tkane igłą, Słowianie - Wikingowie
oto jak to sie robi
Jak to się robi w Niemczech - Podatek kościelny - zasady, Katechistan - kraju Polan powstań z kolan
Katarzyna Gontarczyk Wewnętrzny PR czyli jak to się robi w Sheratonie
Jak to sie robi w Linuksie 2
Jak to sie robi na wolce

więcej podobnych podstron