Platformówka - jak to się robi?

24.11.2009 - Marcin Milewski
TrudnośćTrudność

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:

Trójkąt w oknie

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 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
18
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?
};

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.

Trójkąt w oknie
4.57143
Twoja ocena: Brak Ocena: 4.6 (7 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com