niedziela, 26 września 2010

Klasa Container

GUI jest już prawie funkcjonalne! Teraz dodamy klasę porządkującą trochę strukturę interfejsu naszej aplikacji.

class NGUIContainer : public NGUIBox
{
  NGUIBox **components;
  unsigned int ComponentsNum;
  NGUIBox * focused;

 public:
  NGUIContainer();
  ~NGUIContainer();
  virtual void Draw();
  virtual void DoLogic();
  virtual void OnMouseDown(int x, int y, int button = SDL_BUTTON_LEFT);
  virtual void OnMouseUp(int x, int y, int button = SDL_BUTTON_LEFT);
  virtual void OnMouseMotion(int x, int y);
  virtual void OnKeyDown(SDLKey sym, SDLMod mod);
  virtual void OnKeyUp(SDLKey sym, SDLMod mod);
  void RegisterComponent(NGUIBox * comp);
  void DeregisterComponent(NGUIBox * comp);
  void DeregisterLastComponent();
};

Jedziemy od góry.
Konstruktor zeruje zmienne, destruktor nie robi nic.
Funkcja Draw wypełnia kolorem warstwę Containera, następnie wywołuje funkcję draw wszystkich dzieci Containera i Blituje ich warstwy na swoją.
Funkcja DoLogic nie robi nic.
Kolejne 5 funkcji przekazuje eventy do dzieci Containera, nie chce mi się tego tłumaczyć dogłębnie, bo jest proste.
Funkcja RegisterComponent dodaje dany GUIBox jako dziecko Containera.
Funkcja DeregisterComponent robi dokładnie odwrotnie.
Funkcja DeregisterLastComponent robi to co ta wyżej, tylko że usuwa z listy ostatni komponent, a nie konkretny.

Można powoli obmyślać wygląd interfejsu...

FPS po raz drugi

Posłuchałem rady Fiołka i zmieniłem sposób sterowania szybkością wykonywania się programu.

Nowa sprawa, nowa klasa. Nie miałem pomysłu na nazwę, więc wziąłem pierwszą lepszą na na którą wpadłem: StaticFramerate.
class StaticFramerate
{
    private:
        int dt;
        int accumulator;
        int lastUpdateTime;
        int TIME_STEP;
    public:
        StaticFramerate();
        ~StaticFramerate();
        void update();
        bool isAccumulated();
};
Co tu dużo mówić. funkcja Update ustawia pomocniczą zmienną dt na ilosć milisekund od ostatniego sprawdzenia, odświeża zmienną z czasem ostatniego sprawdzenia i dodaje
wartość dt do accumulatora.

Funkcja isAccumulated sprawdza czy trzeba odświeżyć logikę gry (tzn. ustaliłem że gra ma się odświeżać 60 razy na sekundę. Jeśli od ostatniego sprawdzania minęło przynajmiej 60, 120, 180 milisekund, odświeżamy logikę gry raz, dwa, trzy itd.) i odejmuje od accumulatora 60 milisekund.

Tak wygląda teraz main:
//FPS stuff
 StaticFramerate * FrameStuff = new StaticFramerate;

 for (int frame = 0; ; frame++)
 {
     FrameStuff->update();
  if (Listener->HandleEvents(&event) == -1)
   break;

        while(FrameStuff->isAccumulated())
        {
            Window->DoLogic();
        }
        Window->DrawScreen();
        Screen->Flip();
 }

piątek, 17 września 2010

Klasa Timer - ograniczenie FPS

W obecnym stanie szybkość gry zależy od mocy sprzętu. Jeśli będzie za szybko, nie będzie dało się grać! Aby ograniczyć ilość klatek na sekundę wspomożemy się klasą Timer.

class Timer
{
    private:
    //The clock time when the timer started
    int startTicks;

    //The ticks stored when the timer was paused
    int pausedTicks;

    //The timer status
    bool paused;
    bool started;

    public:
    //Initializes variables
    Timer();

    //The various clock actions
    void start();
    void stop();
    void pause();
    void unpause();

    //Gets the timer's time
    int get_ticks();

    //Checks the status of the timer
    bool is_started();
    bool is_paused();
};

Funkcja start sprawdza, czy timer przypadkiem nie został już uruchomiony. Jeśli nie, ustawiamy started na "true", a startTicks na wartość SDL_GetTicks() (zwraca ona ilość milisekund od zainicjowania biblioteki SDL).

Funkcja stop ustawia started i paused na "false".

Funkcja pause sprawdza, czy timer jest uruchomiony i czy nie jest już zpauzowany. Jeśli nie, zapisujemy do pausedTicks obecną ilość milisekund. Dzięki temu w funkcji unpause przestawimy startTicks na ilość milisekund, przez które Timer był zpauzowany, dając poprawny wynik w get_ticks.

Mała zmiana w main.cpp i gotowe!
Timer *fps = new Timer;

for (int frame = 0; ; frame++)
{
        fps->start();

 if (Listener->HandleEvents(&event) == -1)
  break;

 Window->DoLogic();
 Window->DrawScreen();

 Screen->Flip();

        if(fps->get_ticks() < 1000 / FRAMES_PER_SECOND)
        {
            //Sleep the remaining frame time
            SDL_Delay((1000/FRAMES_PER_SECOND) - fps->get_ticks());
        }
}

Sprawdzamy czy wygenerowanie jednej klatki zajęło MNIEJ niż 60 milisekund. Jeśli tak, musimy chwilę poczekać.

czwartek, 16 września 2010

Klasa Listener

Dodamy do naszej aplikacji EventListener. Nareszcie program nie będzie "udawał" zawieszonego. To jedziemy!
class NGUIEventListener
{
 public:
  NGUIBox *MainContainer;
  void Init(NGUIBox *container);
  int HandleEvents(SDL_Event *event);
};

Jeszcze prostsze niż poprzednie klasy. Funkcją Init przypisujemy do Listenera element GUI, który będzie otrzymywał eventy jako pierwszy. HandleEvents jest ciekawsze odrobinę.
int NGUIEventListener::HandleEvents(SDL_Event *event)
{
    bool exit = false;
    while(SDL_PollEvent(event))
    {
        if (event->type == SDL_QUIT)
            exit = true;
        else
            NGUIEventListener::MainContainer->HandleEvent(event);
    }

    if (exit)
        return -1;
    else
        return 0;
}

Funkcja pobiera sobie po kolei wszystkie eventy wysłane aplikacji przez system, następnie sprawdza czy eventem jest SDL_Quit (Wciśnięcie czerwonego przycisku z Xem, wiadomo o co chodzi), jeśli tak, to zwraca -1. W każdym innym wypadku "pompuje" event do głównej klasy GUI.
Po co ma zwracać -1? A no dlatego:
//Declare SDL_Event struct for use
SDL_Event event;

for (int frame = 0; ; frame++)
{
 if (Listener->HandleEvents(&event) == -1)
  break;

 Window->DoLogic();
 Window->DrawScreen();

 Screen->Flip();
}
//Blabla return 0 koniec maina

Okno już nie zamyka się po 2 sekundach, a czeka aż naciśniemy X.

W kolejnym odcinku: będziemy ograniczać prędkość gry do 60 FPS.

Do następnego razu!

sobota, 4 września 2010

Klasa Window

Kolejna prosta, ale tym razem użyteczna klasa.
class NGUIWindow : public NGUIBox
{
        NGUIBox * MainComponent;

    public:
        void InsertMainComponent(NGUIBox * Component);
        void DrawScreen();
        virtual void DoLogic();
        virtual void HandleEvent(SDL_Event *event);
};
Teraz od początku.

Funkcja InsertMainComponent, jak sama nazwa wskazuje, "łączy" główny komponent naszej aplikacji z klasą Window, dzięki czemu nigdy więcej nie trzeba się martwić o rysowaniu na ekran.
Między wierszami funkcja ta ustawia rozmiary "warstwy" Window'a na całą powierzchnię okna programu, co jest chyba logiczne.

DrawScreen'a raczej nie muszę opisywać. Wywołuje funkcję Draw MainComponentu, nakłada jego warstwę na swoją, a potem rysuje się na ekran.

DoLogic wywołuje funkcję DoLogic MainComponentu.

HandleEvent przekazuje odebrany event do MainComponentu.

To by było na tyle. Proste, nie?

do main.cpp dodałem prosty test tego co do tej pory napisałem. Jeśli okno ekranu wypełnione jest bielą, znaczy to że wszystko działa jak należy :D

środa, 1 września 2010

Pierwszy kawałek kodu: NGUIBOX

Kod tej klasy jest prosty, ale krótki opis jest niżej.
class NGUIObject
{
 public:
        NGUIObject *Parent;
};

class NGUIBox : public NGUIObject
{
 public:
  SDL_Rect Dimensions;
  SDL_Surface * Canvas;
  Uint32 Color;

  NGUIBox();
  ~NGUIBox();

  virtual void Draw();
  virtual void SetDimensions(int x, int y, int w, int h);
  void ChangeColor(Uint32 color);
  void Blit(SDL_Surface *dest);
  virtual void HandleEvent(SDL_Event *event);
  virtual void DoLogic();
  virtual void OnMouseDown(int x, int y, int button = SDL_BUTTON_LEFT);
  virtual void OnMouseUp(int x, int y, int button = SDL_BUTTON_LEFT);
  virtual void OnMouseMotion(int x, int y);
  virtual void OnKeyDown(SDLKey sym, SDLMod mod);
  virtual void OnKeyUp(SDLKey sym, SDLMod mod);
};


Klasę NGUIObject dodałem na wszelki wypadek, prawdopodobnie zostanie wywalona, a "Parent" przeniesiony do Box'a. Wskaźnik ten pełni dość ważną funkcję - dzięki niemu będzie można się w innych klasach GUI odwoływać się do Containera, w których siedzą. Przydatne, jeśli potraktujemy jako całość np. menu z przyciskami.

Jedziemy od końca. Funkcje aż do DoLogic włącznie nie robią w Box'ie nic. Są wirtualne, gdyż obiekty dziedziczące po tejże klasie różnie mogą reagować na "bodźce" od użytkownika, a funkcja DoLogic została dodana...a w sumie nie wiem po co :P. Narazie nie wymyśliłem zastosowania.

Ponieważ struktura SDL_event opisuje wszystkie rodzaje eventów, jakie może rejestrować SDL, przydało by się jakieś rozgraniczenie. Funkcja ta sprawdza, jaki rodzaj eventu jest w tym momencie przenoszony, a potem wywołuje odpowiednią funkcję (z tych opisanych wyżej).

Funkcja Blit nakłada warstwę(zacznę tak nazywać SDL_Surface, ok?) Canvas na podaną w argumencie. W ten sposób kiedyśtam zobaczymy ją na ekranie.

Funkcja ChangeColor istnieje w sumie tylko do debugu - wiadomo czy dana klasa GUI wogóle się rysuje.

Funkcja SetDimensions wpakowuje podane rozmiary do Dimensions, a potem tworzy z nimi nową warstwę (gdyż nie da się, a przynajmiej nie wiem jak, zmienić rozmiarów istniejącej warstwy). Sprawdza przy okazji, czy wcześniej już warstwy nie zrobiliśmy, jeśli tak to usuwamy starą (zapobiegamy wyciekowi pamięci).

Funkcja Draw odświeża zawartość warstwy Canvas, wywołujemy ją w każdej klatce.

Jest późno, więc nie zdążyłem napisać demka do main.c, zrobię to przy następnym apdejcie.

sobota, 28 sierpnia 2010

Planowanie GUI

Zapewne zadziwia was tytuł wpisu. "Jak to, na ekranie nie widać nawet ludzika czy jakiegokolwiek obrazka, a on chce robić GUI??". Jednak nie jest to (aż tak)głupi krok na jaki wygląda, a już wyjaśniam dlaczego.

Zamierzam teraz napisać tylko szkielet, a mianowicie:

- klasę BOX, z której będą dziedziczyć wszystkie elementy interfejsu. Posiada swój własny SDL_Surface. Będzie to zwykły prostokąt z możliwością pokolorowania i wbudowanymi funkcjami odbierającymi eventy (OnMouseDown, OnMouseUp, OnMouseMotion itd.)

- klasę CONTAINER, która będzie przechowywać wskażniki do innych klas GUI, zajmować się przerysowywaniem Surfejsów "dzieci" na siebie i przekazywaniem eventów tam, gdzie trzeba.

- klasę WINDOW, która będzie "pakować" eventy w swoje bebechy (do jedynej klasy-dziecka, najrozsądniej wstawić tu CONTAINERa oczywiście :P) i rysować się na ekran.

Dzięki takiemu rozwiązaniu klasa rysująca świat gry może być klasą GUI, co eliminuje późniejszy problem "hardkodowania" przycisków na ekranie.

Klasy jak BUTTON czy PROGRESS_BAR dopiszę później, jak ludzik będzie mógł eksplorować świat :)

Niestety jestem zajęty przez te 2 dni i nie zrobię tego teraz, więc see ya poniedziałek.