Gra 2D, część 4: Piszemy Hall of Fame

29.01.2010 - Łukasz Milewski
TrudnośćTrudność
Każdy z nas lubi wygrywać, być najlepszym ;-) Dlatego, aby uatrakcyjnić naszą grę powinniśmy zaspokoić tę potrzebę gracza. W tej części kursu stworzymy ekran, na którym gracz będzie mógł umieścić swoje imię gdy zdobędzie więcej punktów niż inni.

Poprzedni artykuł - mapa kaflowa Następny artykuł - odtwarzamy dźwięk

Plan działania

Skonstruujemy Hall of fame, czyli listę najlepszych graczy, posortowaną według ilości punktów. Na początek nauczymy się korzystać z czcionek bitmapowych. Następnie na podstawie takiej czcionki wyświetlimy prosty hall of fame. Ostatnim krokiem będzie zaprogramowanie ekranu, na którym można wpisać swoje imię. Zaprogramujemy dwa sposoby wpisywania imienia: z klawiatury lub klikając na odpowiednie litery. Zaczynamy!

Kroki początkowe

Wgraj sobie nową teksturę. Umieść ją w katalogu data. Zawiera ona wszystko to, co poprzednio oraz czcionkę. Zauważ, że napisy są na szachownicy. Dzięki temu łatwiej jest narysować litery. Grafika jest robocza, co oznacza że zmienimy ją gdy gra będzie gotowa. Tutaj znajdziesz kod, od którego zaczynamy.

Czcionka bitmapowa

Pierwszym efektem będzie czcionka rastrowa (znana również jako czcionka bitmapowa). Oznacza to, że wyświetlane litery są zapisane w pliku z grafiką jako bitmapa. Naszym celem jest wyświetlanie słów złożonych z liter i cyfr oraz znaków specjalnych.

Zacznijmy od zdefiniowania interfejsu klasy Text. Chcemy mieć możliwość ustawienia wielkości tekstu oraz warstwy, na której będzie on widoczny. Chcielibyśmy także móc narysować literę, cyfrę, znak specjalny (np. podkreślenie), cały napis oraz liczbę. Dodatkowo, potrzebujemy specjalnej metody rysującej, aby nie powtarzać niepotrzebnie kodu. Kod może wyglądać np. tak (plik Text.h):

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
#ifndef __TEXT_H__
#define __TEXT_H__
 
class Text {
public:
    explicit Text(double width = 0.025, double height = 0.025, size_t layer = 0) {
        SetSize(width, height);
        SetLayer(layer);
    }
 
    void SetSize(double width, double height) {
        m_width = width;
        m_height = height;
    }
 
    void SetLayer(size_t layer) {
        m_layer = layer;
    }
 
    void DrawDigit(char ch, double pos_x, double pos_y);
    void DrawLetter(char ch, double pos_x, double pos_y);
    void DrawSpecial(char ch, double pos_x, double pos_y);
    void DrawText(const std::string& text, double pos_x, double pos_y);
    void DrawNumber(size_t number, double pos_x, double pos_y, size_t width = 0);
 
private:
    void Draw(int tex_x, int tex_y, double pos_x, double pos_y);
 
private:
    double m_width;
    double m_height;
    size_t m_layer;
};
 
#endif /* __TEXT_H__ */
  

Parameter width w DrawNumber określa, jaka ma być minimalna długość liczby (gdy jest zbyt krótka, wyrównujemy ją odstępami z lewej strony). Przejdźmy do implementacji. Potrzebujemy funkcji, która zamienia liczbę na string (można to zrobić łatwo przy pomocy boost::lexical_cast, ale staramy się ograniczyć wykorzystanie boost do klasy shared_ptr). Do pliku Utils.h wpisujemy taką treść:

1
2
3
4
5
6
7
8
9
10
11
#ifndef __UTILS_H_INCLUDED__
#define __UTILS_H_INCLUDED__
#include <sstream>
#include <string>
 
/**
 * Zamienia liczbę number na  odpowiadający jej ciąg znaków.
 */
std::string IntToStr(int number);
 
#endif
A do pliku Utils.cpp:
1
2
3
4
5
6
7
#include "Utils.h"
 
std::string IntToStr(int number) {
    std::stringstream ss;
    ss << number;
    return ss.str();
}

Następny krok to rysowanie. Zajdzie potrzeba manipulowania macierzą projekcji, dlatego z klasy App przenosimy fragment metody App::Resize. Obecnie powinna ona wyglądać tak:

1
2
3
4
5
6
7
8
9
10
11
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;
 
    Engine::Get().Renderer()->SetProjection(width, height);
}

Ustawienie macierzy projekcji jest teraz w klasie Renderer. Do pliku Renderer.cpp na koniec dopisujemy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Renderer::SetProjection(size_t width, size_t height) {
    glViewport(0, 0, 
               static_cast<GLsizei> (width), 
               static_cast<GLsizei> (height));
    ResetProjection();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}
 
void Renderer::ResetProjection() {
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, 1, 0, 1, -1, 10);
}

a do pliku Renderer.h do klasy Renderer w zasięgu publicznym dorzucamy deklaracje:

1
2
    void SetProjection(size_t width, size_t height);
    void ResetProjection();

Znaczenie tych kodów zostało wytłumaczone w artykule Tworzenie okna i wyświetlanie animowanego sprite'a

Spójrzmy na metodę Text::Draw. Jej zadaniem jest wyświetlenie na ekranie sprite'a z atlasu tekstur z pozycji (tex_x, tex_y) na pozycję (pos_x, pos_y). Metoda ta zakłada, że sprite jest wielkości (32,32) oraz ma być wyświetlony na ekranie w wielkości ustawionej metodą SetSize (pola m_width, m_height).

Wyświetlanie sprite'a jest w pewien sposób specjalne Jest on bowiem wyświetlany tak, jakby (0,0) było w lewym dolnym rogu ekranu, a (1,1) w prawym górnym rogu. Inaczej jest wyświetlana mapa i gracz. Dla gracza pozycja (0,0) oznacza, że jest on na początku mapy. Nie znaczy to jednak, że jest w lewym dolnym rogu ekranu!

Aby uzyskać taki efekt, musimy ustawić macierz projekcji na identyczność oraz rzut ortogonalny od początku (tak, jak w artykule pierwszym - metoda App::Resize). Zanim to jednak zrobimy, powinniśmy odłożyć na stos stare macierze - MODELVIEW i PROJECTION. Nie chcemy przecież, aby Text::Draw wpływało w jakikolwiek sposób na rysowanie innych elementów (np. przesuwało je).

Przydatna jest struktura danych, która pozwala dodawać elementy i je z niej pobierać. Chcemy, aby pobrany element był zawsze tym najpóźniej włożonym (elementy wyjmujemy w kolejności odwrotnej do wkładania). Można to przyrównać do stosu kartek. Zawsze na górze jest tylko jedna - ta ostatnio położona. Aby zobaczyć kolejną kartkę trzeba podnieść pierwszą. Struktura, która zachowuje się w ten sposób nazywana jest stosem. W naszym przypadku na stos odkładamy macierze OpenGL.

Aby odłożyć macierz na stos, należy ustawić ją jako aktywną - przez glMatrixMode. Następnie możemy posłużyć się funkcjami glPushMatrix i glPopMatrix do obsługi stosu macierzy.

Po ustawieniu macierzy projekcji od nowa, a macierzy MODELVIEW na identyczność, wystarczy narysować sprite'a przy pomocy znanej nam już metody Renderer::DrawSprite.

Pokaż/ukryj kod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Text::Draw(int tex_x, int tex_y, double pos_x, double pos_y) {
 
    glPushMatrix(); // MODELVIEWn
    {
        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        {
            Engine::Get().Renderer()->ResetProjection();
            glMatrixMode(GL_MODELVIEW);
            glLoadIdentity();
            Engine::Get().Renderer()->DrawSprite(tex_x, tex_y, 32, 32, 
                                                 pos_x, pos_y, m_width, m_height,
                                                 DL::DisplayLayer(m_layer));
        }
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
    }
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
}
  

Posiadając metodę Draw, możemy zdefiniować kolejne proste metody: DrawDigit, DrawLetter, DrawSpecial. Kolejno wypisują one cyfry, litery oraz znaki specjalne (np. podkreślenie). Zadanie tych metod sprowadza się do obliczenia, gdzie w atlasie znajduje się odpowiednia grafika i wywołania metody Draw.

Poszczególne znaki są zapisane w pliku tak, jak widać na załączonym obrazku (kolejny raz podkreślmy, że jest to grafika robocza - zostanie zmieniona po ukończeniu kodu gry).

Na podstawie powyższej ilustracji łatwo wyznaczyć pozycje kolejnych liter (w teksturze obrazek z czcionką jest przesunięty w dół).

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
void Text::DrawDigit(char ch, double pos_x, double pos_y) {
    int digit = ch - '0';
    int tex_x = digit * 32;
    int tex_y = 7*32;
    Draw(tex_x, tex_y, pos_x, pos_y);
}
 
void Text::DrawLetter(char ch, double pos_x, double pos_y) {
    int letter = toupper(ch) - 'A';
    
    int letter_row = letter / 10; // wiersz, w którym jest litera
    int letter_col = letter % 10; // kolumna, w której jest litera
 
    int tex_x = letter_col * 32;
    int tex_y = (8+letter_row) * 32;
 
    Draw(tex_x, tex_y, pos_x, pos_y);
}
 
void Text::DrawSpecial(char ch, double pos_x, double pos_y) {
    double tex_x = 0;
    double tex_y = 0;
 
    if (ch == '_') {
        tex_x = 192;
        tex_y = 320;
    }
    else {
        return; // pomijamy znaki, których nie znamy
    }
    Draw(tex_x, tex_y, pos_x, pos_y);
}
  

Na bazie tych metod możemy zbudować kolejne: DrawText oraz DrawNumber.

Text::DrawText iteruje po kolejnych znakach i przy pomocy funkcji z nagłówka cctype sprawdza, czy znak jest literą, cyfrą czy podkreśleniem. Wszystkie inne znaki są pomijane. Gdy metoda pozna typ znaku, od razu wywołuje odpowiednią metodę spośród tych, które przed chwilą wymieniliśmy.

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
void Text::DrawText(const std::string& text, double pos_x, double pos_y) {
    double x = pos_x;
    double y = pos_y;
 
    for (size_t i = 0; i < text.size(); ++i) {
        char ch = text.at(i);
        if (isdigit(ch)) {
            DrawDigit(ch, x, y);
        }
        else if (isalpha(ch)) {
            DrawLetter(ch, x, y);
        }
        else if (ch == '_') {
            DrawSpecial(ch, x, y);
        }
        else {
            ; // inne znaki po prostu pomijamy
        }
        x += m_width;
    }
}
  

Metoda Text::DrawNumber też jest bardzo leniwa. Najpierw przy pomocy fukncji IntToStr zamienia liczbę na napis. Następnie sprawdza, czy liczba jest dobrze wyrównana (dodaje wiodące zera do osiągnięcia długości width). Ostatecznie wywoływana jest metoda DrawText z odpowiednim tekstem, reprezentującym daną liczbę.

1
2
3
4
5
6
7
8
9
10
11
12
13
void Text::DrawNumber(size_t number, 
                      double pos_x, 
                      double pos_y, 
                      size_t width) {
    std::string number_str = IntToStr(number);
    size_t spaces_count = std::max(0, 
                                   static_cast<int> (width) 
                                   -
                                   static_cast<int> (number_str.size()));
    for (size_t i = 0; i < spaces_count; ++i)
        number_str = " " + number_str;
    DrawText(number_str, pos_x, pos_y);
}

Możemy spróbować wypisać coś na ekranie. Wystarczy do metody App::Draw dopisać takie linijki (i dodać nagłówek Text.h). Powinny być dodane zaraz po narysowaniu gracza.

1
2
        Text t;
        t.DrawText("witaj swiecie", 0.3, 0.5);
5
Twoja ocena: Brak Ocena: 5 (3 ocen)

Copyright © 2008-2010 Wrocławski Portal Informatyczny

design: rafalpolito.com