Platformówka - jak to się robi?

24.11.2009 - Marcin Milewski
TrudnośćTrudność

  Następny artykuł - poruszanie postacią

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 (lub trochę czasu na poeksperymentowanie z przykładami). Będziemy też potrzebować bibliotek SDL oraz boost. 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ć:

Efekt końcowy

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
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <SDL/SDL.h>
 
class App {
public:
  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
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
  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
6
7
8
void App::Draw() {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  // tu będziemy coś rysować
  // ...
  
  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

. 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.

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

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com